Rate Limiting and Backpressure for Workers

LESSON

Caching, Workers, and Performance

007 30 min intermediate

Rate Limiting and Backpressure for Workers

By the end of this lesson, you will be able to...

  • Distinguish a rate limit, a concurrency limit, and backpressure in a worker pipeline.

  • Diagnose when a growing queue is protecting a dependency rather than proving workers need to scale.

  • Choose an immediate mitigation and a preventive control from queue, dependency, and retry signals.

Idea in one sentence: A healthy worker system sometimes lets work wait on purpose, because safe sustained completion matters more than emptying a queue as fast as possible.

Core Insight

At 09:00, a product launch creates 8,000 video-transcode jobs. The worker pool is healthy. Someone raises its concurrency from four workers to twenty-four so the queue will drain faster.

For several minutes, the graph looks good: queue depth falls. Then the external encoding provider begins returning 429 Too Many Requests. Retry volume climbs. The database receives a burst of job-status updates. Queue depth starts growing again, now full of failed attempts and new uploads.

The workers were not too slow. They were too successful at creating pressure.

backlog -> more claims -> more provider calls -> 429s -> retries -> larger backlog

Lesson 005 established that a pool limits active work. This lesson adds two other controls:

They are not interchangeable knobs. Together, they stop a buffered burst from becoming a downstream outage.

The Production Symptom

At 09:10 the on-call engineer sees:

Signal Before the launch At 09:10 What it suggests
Transcode queue depth 200 6,400 Arrivals exceed successful completion
Oldest job age 20 seconds 11 minutes Learners may wait beyond the product delay budget
Worker concurrency 4 24 More work is being activated
Provider 429 rate 0 38% The external dependency is refusing the start rate
Retry rate Low Rising rapidly Failures are being fed back into the queue
Database update latency Normal High The retry wave is stressing another dependency

The tempting diagnosis is “we need even more workers.” The evidence says otherwise. The provider is the active limit, and retries are multiplying work. Adding more consumers would make the provider and database see more failed requests, not more useful completion.

The user impact is not only a long queue. A learner may see “processing” for much longer, urgent jobs can wait behind bulk uploads, and the provider incident lasts longer because the platform keeps sending work it cannot accept.

Three Controls, Three Questions

Plain meaning:

Some controls decide how many jobs can be active. Some decide how fast new jobs can start. Backpressure decides what the system does when those limits are already full.

In this scenario:

The platform permits at most 10 active transcodes, starts at most 30 new provider requests per minute, and stops claiming low-priority jobs when the provider rejects or delays too much work.

Technical names:

Control Question it answers Example
Concurrency limit How many expensive operations may be in flight now? At most 10 active encodes
Rate limit How frequently may new operations begin? At most 30 provider starts/minute
Backpressure How does the pipeline change at unsafe capacity? Slow claims, defer low priority work, reserve urgent capacity

Concurrency protects long-lived pressure: CPU, memory, connection pools, and provider sessions. Rate limiting protects burst frequency and quotas. A short request can finish quickly yet still violate a per-minute provider quota. A long request can respect a rate limit yet consume every active slot.

Investigation Path

The on-call response should follow the work path rather than one dashboard number.

queue age rising
-> are workers idle or full?
-> if full, which dependency is slow or rejecting work?
-> are retries increasing the effective arrival rate?
-> which job class must keep moving?
-> apply a temporary bound, then watch useful completion

For the transcode incident:

  1. Confirm that workers are full, not dead.
  2. Observe the provider's 429 errors and latency; it is the current bottleneck.
  3. Lower new starts to the provider-safe rate and cap active encodes.
  4. Reduce retry pressure with delayed, bounded attempts rather than immediate requeueing.
  5. Reserve a small path for urgent instructor fixes, or defer bulk re-encodes.
  6. Watch provider acceptance, completion rate, oldest-job age, and database latency together.

The mitigation may make the queue drain more slowly at first. That is expected. The goal is to restore stable useful throughput: jobs that complete without producing new failed work.

A Worked Control Trace

Assume the provider accepts 30 starts per minute and can sustain 10 active encodes. Jobs average two minutes.

Minute Naive dispatch Provider result Controlled dispatch Result
09:00 80 starts, 24 active Many rejects begin 30 starts, up to 10 active Queue grows, provider stays inside quota
09:01 Retries plus 80 new starts 429 rate increases Only tokens available start; low-priority jobs wait Few new failures enter queue
09:02 Database writes retry state for many failures Update latency rises Completed jobs free slots; next permitted jobs claim Completion is steady
09:10 Queue is noisy and failure-heavy Recovery is harder Queue is still present but aging predictably The system can measure when extra capacity is actually needed

One simplified admission check makes the two budgets visible:

def may_start_transcode(in_flight, limiter):
    if in_flight >= 10:
        return False
    return limiter.try_take_token()  # at most 30 starts per minute

This is not a complete distributed limiter implementation. The important point is that one check protects active work and the other protects start frequency. If either refuses work, the worker does not turn that refusal into an immediate retry storm.

The naive failure contrast is:

naive claim: an empty queue proves the system is healthy
better claim: completed useful work within dependency limits proves the pipeline is healthy

So far, the queue is allowed to hold work. It is not abandoned. Its age, priority, and drain rate are now part of an explicit product and operational contract.

The Failure Mechanism: Retries Can Defeat a Limit

Suppose every provider 429 is immediately requeued. The rate limiter may allow 30 first attempts per minute, but failed work returns at the same time as new work. Without delayed and bounded retries, the effective arrival rate rises just when capacity fell.

provider rejects
-> retry immediately
-> more claims compete with fresh jobs
-> more provider rejects
-> work multiplies without completion

Backpressure changes that loop. It can pause claims briefly, delay retries, reduce the producer's nonessential job creation, and protect a priority lane. The exact retry and dead-letter semantics are developed later in the track. Here, the operational rule is enough: failed work must not bypass the same safety budget that protected the first attempt.

Mitigation and Prevention

Immediate mitigation

Prevention

This is not a call to keep limits permanently low. It is a call to increase them only with evidence that the limiting dependency can sustain the extra pressure.

Signals to Watch

One chart rarely explains worker overload. Use a small set that connects cause and outcome:

A good alert combines a symptom with a time budget. “Queue has 1,000 jobs” is vague. “Oldest reminder exceeds 10 minutes while completion rate falls and provider rejections rise” tells an operator which path is failing and why it matters.

Trade-offs and Limits

The central trade-off is immediate drain speed versus dependency safety and recovery. A tighter limit makes some jobs wait longer. It also prevents failed starts, retries, and cascading latency from consuming more capacity than the successful work.

Backpressure is useful when the system can postpone or prioritize work. It does not make an indefinitely overloaded service meet every deadline. If arrival rate remains above sustainable completion for long enough, the team needs a product decision, more safe downstream capacity, or admission control at the producer boundary.

Limits can fail too. A global counter might be unavailable, a threshold may be too strict, or an emergency priority lane can starve normal work if it becomes the default. You see the boundary when oldest-job age keeps climbing despite stable limits, provider errors stay high under the safe rate, or one class never receives its share of capacity.

Common Confusions

Confusion: Rate and concurrency limits are the same control

Why it is tempting:

Both can make workers start fewer jobs.

Better model:

Concurrency limits active work now. Rate limits starts over time. Long jobs can exhaust concurrency without exceeding a rate; fast jobs can exceed a rate without many active operations.

Confusion: A long queue proves the system is broken

Why it is tempting:

Waiting work is visible and uncomfortable.

Better model:

A queue can be a healthy buffer during a finite burst. Evaluate its age, priority, and drain behavior alongside dependency signals. An empty queue created by rejected work is worse than a bounded queue that completes steadily.

Confusion: Backpressure means stop all work

Why it is tempting:

“Slow down” sounds like a global pause.

Better model:

Backpressure can be selective: reduce one dependency's starts, defer low-priority jobs, or reserve capacity for urgent work. The response should match the scarce resource and the product promise.

Check Your Understanding

Check: A provider permits 30 starts per minute. Jobs now finish in five seconds, and workers begin 80 jobs per minute while only three are active at any moment. Which control is missing?

Think first, then reveal.

Answer: A rate limit. Low active concurrency does not prevent too many fast starts within the provider's minute-level quota.

Check: Queue depth rises during a launch, provider errors are zero, completion rate remains stable, and oldest-job age stays below the promised delay. Must the team add workers immediately?

Think first, then reveal.

Answer: No. The queue may be absorbing a finite burst safely. Add capacity only after checking whether the sustained arrival rate and user delay budget require it.

Check: 429 errors and retries spike after raising worker concurrency. What is the safer first mitigation?

Think first, then reveal.

Answer: Reduce active starts to the provider's safe budget, delay retries, and stop low-priority claims. Adding more workers would increase the rejected work path.

Practice

An email provider accepts 100 sends per minute and 20 active connections. Your platform has password resets, class reminders, and a bulk marketing campaign in one worker queue. During the campaign, password resets are delayed.

Propose a control policy. Include:

A strong answer reserves capacity for password resets, gives marketing a paced low-priority path, delays rather than immediately retries rejected sends, and measures urgent-job age plus provider errors and useful completion. It acknowledges that persistent demand beyond provider capacity needs a product or provider-capacity decision.

Resources

Key Takeaways

  1. Concurrency, rate, and backpressure answer different questions. Use each to control active pressure, start frequency, and overload response.
  2. A queue can be healthy waiting work. Judge it by age, priority, completion, and dependency health—not by depth alone.
  3. Retries are load. A failed attempt must obey the same pressure budget as a new one.
  4. Sustainable completion is the goal. A slower stable pipeline beats a fast path that creates rejection and cascades.
PREVIOUS Scheduling, Delayed Jobs, and Time-Based Work NEXT Observability for Worker Systems