Camel Throttler & Rate Limiter: Back Pressure in Integration Routes

Integration routes can overwhelm downstream services when message volume spikes. The Throttler EIP limits the rate of message flow, letting excess messages wait in a queue rather than crashing a downstream API. Combined with the Delay EIP for fixed pauses, circuit breakers for fast-fail, and dynamic rate expressions, Camel gives you fine-grained back-pressure control across all integration scenarios.

Contents

throttle(): Requests per Period

throttle(N) allows at most N messages to pass through per time period (default 1 second). Excess messages are held and released in order when capacity is available.

Java
@Component
public class ThrottledApiRoute extends RouteBuilder {

    @Override
    public void configure() {
        from("direct:callExternalApi")
            // Allow at most 10 messages per second to the external API
            .throttle(10)
                .timePeriodMillis(1_000)    // default is 1000ms
            .to("http:api.example.com/endpoint");

        // Per minute rate limit
        from("direct:sendEmail")
            .throttle(100)
                .timePeriodMillis(60_000)   // 100 emails per minute
            .to("smtp:mail.example.com");
    }
}

Async Throttling

By default, throttling blocks the calling thread. Use asyncDelayed(true) to release the calling thread immediately and schedule delivery asynchronously, preventing thread starvation under high load.

Java
from("kafka:api.requests")
    .throttle(50)
        .timePeriodMillis(1_000)
        .asyncDelayed()               // non-blocking — releases the Kafka consumer thread
        .executorService(Executors.newFixedThreadPool(10))
    .to("http:downstream-service/api");

Dynamic Rate Limits

The throttle rate can be an expression evaluated per message: useful for tenant-based rate limits or header-driven throttling.

Java
// Rate limit from a header (set by upstream auth/routing)
from("direct:tenantRequest")
    .throttle(header("X-Rate-Limit"))          // header contains the allowed rate
        .timePeriodMillis(1_000)
    .to("direct:processRequest");

// Rate limit from a bean (e.g., read from a database or config service)
from("direct:apiCall")
    .throttle(method("rateLimitService", "getLimit"))  // dynamic bean lookup
        .timePeriodMillis(1_000)
    .to("http:external-api");
Java
@Component("rateLimitService")
public class RateLimitService {

    private final RateLimitRepository repo;

    // Called per message to determine current allowed rate
    public long getLimit(Exchange exchange) {
        String tenantId = exchange.getIn().getHeader("tenantId", String.class);
        return repo.findByTenantId(tenantId)
                   .map(RateLimit::getRequestsPerSecond)
                   .orElse(10L);  // default 10 rps
    }
}

delay(): Fixed & Expression-Based Pauses

The delay() EIP introduces a fixed wait before forwarding the message: useful for polling intervals, retry back-off, or pacing batch jobs.

Java
// Fixed 500ms delay between messages
from("direct:batchProcessor")
    .delay(500)
    .to("direct:callApi");

// Expression-based delay — exponential back-off using a header
from("direct:retryWithBackoff")
    .delay(simple("${header.retryCount} * 1000"))  // 1s, 2s, 3s, ...
    .to("direct:callFlakyService");

// Async delay — don't block the thread
from("direct:scheduledTask")
    .delay(2_000)
        .asyncDelayed()
    .to("direct:processTask");

Per-Correlation Throttling

Throttle independently per key: e.g., limit each tenant to 5 requests/second without affecting other tenants.

Java
from("direct:multiTenantApi")
    .throttle(5)
        .timePeriodMillis(1_000)
        .correlationExpression(header("tenantId"))  // separate quota per tenant
    .to("http:backend-service/api");

Circuit Breaker with Resilience4j

The circuit breaker complements throttling: while the throttler controls rate, the circuit breaker stops sending entirely when a downstream service is failing.

XML
<dependency>
  <groupId>org.apache.camel.springboot</groupId>
  <artifactId>camel-resilience4j-starter</artifactId>
</dependency>
Java
from("direct:callPaymentGateway")
    .throttle(20).timePeriodMillis(1_000)   // max 20 rps
    .circuitBreaker()
        .resilience4jConfiguration()
            .slidingWindowSize(10)
            .failureRateThreshold(50)       // open circuit if 50% fail
            .waitDurationInOpenState(10_000) // wait 10s before half-open
        .end()
    .to("http:payment-gateway/charge")
    .onFallback()
        .log(LoggingLevel.WARN, "Circuit open — payment gateway unavailable")
        .setBody(constant("{\"status\":\"PENDING\",\"message\":\"Payment deferred\"}"))
    .end();

Back Pressure Patterns

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
// Pattern: bounded queue + throttle + dead letter on overflow
from("kafka:order.events")
    .throttle(100)
        .timePeriodMillis(1_000)
        .asyncDelayed()
        .rejectExecution(true)   // reject (instead of queue) when executor is full
    .to("direct:processOrder")
    .onException(RejectedExecutionException.class)
        .handled(true)
        .to("kafka:order.events.overflow");  // push back for later retry

// Pattern: sample — process only 1 in N messages (lossy but zero latency)
from("kafka:sensor.highfreq")
    .sample()
        .samplePeriod(1)
        .units(TimeUnit.SECONDS)    // process at most 1 message per second per partition
    .to("direct:aggregateSensor");

Throttler vs Delay vs Circuit Breaker

EIPPurposeMessages Dropped?Use When
throttle()Limit messages per time windowNo: queuedDownstream has a known rate limit (API quota)
delay()Fixed pause before forwardingNo: delayedPacing, retry back-off, polling intervals
sample()Lossy rate reductionYes: sampledHigh-frequency sensor data where loss is acceptable
circuitBreaker()Stop sending on failureYes: fallbackDownstream is unreliable; prevent cascade failure