Queue producers and consumers

Tune batches, delay, and concurrency

Balance throughput, latency, downstream capacity, and retry cost without overwhelming the service doing the real work.

8 minute lesson

~~~

A queue consumer receives messages in batches, and three knobs in the consumer configuration control how work flows:

{
  "queues": {
    "consumers": [
      {
        "queue": "email-jobs",
        "max_batch_size": 25,
        "max_batch_timeout": 5,
        "max_retries": 5,
        "max_concurrency": 10,
        "dead_letter_queue": "email-jobs-dlq"
      }
    ]
  }
}

max_batch_size and max_batch_timeout work as a pair: deliver when 25 messages are waiting, or after 5 seconds, whichever comes first. Batch size and wait time trade immediate delivery for efficient processing. Big batches amortize setup — one warm connection sends 25 emails cheaply — while a long timeout on a quiet queue means every message waits. Latency-sensitive queues want small numbers; bulk pipelines want large ones.

max_concurrency caps how many consumer invocations run at once. The platform scales consumers up as backlog grows, and this setting is your brake. Consumer concurrency increases throughput until the database, API, or CPU boundary becomes the bottleneck — after that, more concurrency just moves the queue into your database. If the email provider allows 100 requests per second and each invocation sends 10 per second, concurrency above 10 buys you nothing but 429 responses.

Delay is for known futures

Producers and retries can postpone delivery:

await env.EMAIL_JOBS.send(job, { delaySeconds: 600 })
// or, in the consumer, on a transient failure:
message.retry({ delaySeconds: 30 * 2 ** (message.attempts - 1) })

Delayed delivery is useful for a known future attempt — try again in ten minutes, when the rate-limit window resets — not indefinite workflow state. “Park this until the user approves” is a Workflow’s job.

Add backoff and jitter to repeated failures. The doubling above spaces attempts out; adding a little randomness stops a thousand messages that failed together from retrying in the same second and knocking the dependency over again.

Tune from evidence

Tune from queue backlog, processing duration, error rate, and downstream limits — not from guesses. Backlog growing while consumers idle points at concurrency or batch size. Error rate rising with concurrency means you’ve found a downstream ceiling; back off below it.

Then verify: load-test a practice consumer with a deliberately slow dependency and find the concurrency that stays below its capacity. Watching backlog drain smoothly at concurrency 8 and explode with errors at 20 teaches you where the real limit is, on a queue where mistakes are free.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →