---
title: "14 — Asynchronism: Message Queues, Task Queues, and Back Pressure"
uid: asynchronism-and-queues
tags: ["back-pressure", "message-queue", "task-queue", "asynchronism", "roadmap:system-design", "system-design"]
excerpt: "A queue is a buffer that decouples producers from consumers — and back pressure is how a full queue protects the system instead of drowning it."
date: 2026-08-13T03:27:32+0000
source: https://www.aveshina.my.id/en/blog/asynchronism-and-queues
---

"Just background it" was my queue strategy, and it skipped the two decisions that make the pattern safe at scale. Writing it down gave the pattern a precise shape: **a queue is a buffer that decouples the producer (who hands off work) from the consumer (who does it), so the producer returns fast and the consumer works at its own pace.** [1][3] The two things that make that decoupling safe at scale are choosing the right queue type and applying back pressure when the buffer fills.

The framing that landed is that asynchronism exists to fix a mismatch in _rate_. A user request arrives in milliseconds; the work it triggers (encoding a video, fanning out a notification, running a report) takes seconds or minutes. Holding the request open for the work to finish ties up a thread and exhausts the server under load. A queue absorbs the rate mismatch — the producer drops a message and leaves; the consumer pulls messages at whatever rate it can sustain [1].

## Two queue types for two jobs

The roadmap distinguishes message queues from task queues, and the distinction is real [3][4]:

```figure
<svg viewBox="0 0 740 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two queue types. Left, Message Queue: producers publish messages, a worker consumes them — fire-and-forget notification of an action. Right, Task Queue: producers submit tasks with input data, a worker runs them and delivers results — computationally-intensive jobs.">
  <defs>
    <marker id="aqarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Message Queue -->
    <text x="185" y="24" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">Message Queue</text>
    <text x="185" y="40" font-size="10" fill="#64748b" text-anchor="middle">receive, hold, deliver messages</text>
    <rect x="30" y="60" width="80" height="34" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="70" y="82" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">producer</text>
    <rect x="150" y="60" width="100" height="34" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="200" y="82" font-size="10" font-weight="700" fill="#422006" text-anchor="middle">queue</text>
    <rect x="290" y="60" width="80" height="34" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="330" y="82" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">worker</text>
    <path d="M110,77 L148,77" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aqarrow)"/>
    <path d="M250,77 L288,77" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aqarrow)"/>
    <text x="185" y="120" font-size="10" fill="#1e1b4b" text-anchor="middle" font-style="italic">"an action happened" — notify, fan out</text>
    <text x="185" y="138" font-size="10" fill="#1e1b4b" text-anchor="middle">e.g. tweet published → fan out to followers</text>

    <!-- Task Queue -->
    <text x="555" y="24" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">Task Queue</text>
    <text x="555" y="40" font-size="10" fill="#64748b" text-anchor="middle">receive tasks + data, run, deliver results</text>
    <rect x="430" y="60" width="80" height="34" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="470" y="82" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">producer</text>
    <rect x="520" y="60" width="100" height="34" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="570" y="82" font-size="10" font-weight="700" fill="#500724" text-anchor="middle">task queue</text>
    <rect x="640" y="60" width="80" height="34" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="680" y="82" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">worker</text>
    <path d="M510,77 L518,77" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aqarrow)"/>
    <path d="M620,77 L638,77" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aqarrow)"/>
    <text x="555" y="120" font-size="10" fill="#500724" text-anchor="middle" font-style="italic">"do this expensive thing" — compute</text>
    <text x="555" y="138" font-size="10" fill="#500724" text-anchor="middle">e.g. resize upload, train model, render PDF</text>

    <!-- Back pressure -->
    <rect x="150" y="180" width="440" height="56" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="370" y="204" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">Back pressure</text>
    <text x="370" y="222" font-size="10" fill="#7f1d1d" text-anchor="middle">queue full → producer gets "busy" (HTTP 503) → retry with exponential backoff</text>
  </g>
</svg>
```

- **Message queues** receive, hold, and deliver messages. When an operation is too slow to perform inline, the workflow is: the application publishes a job to the queue and notifies the user of status; a worker picks up the job, processes it, then signals completion [4]. The user is not blocked. During processing, the client might do a little optimistic work to make the task _seem_ done — a posted tweet appears in the poster's timeline instantly, even if it takes time to fan out to all followers [4].
- **Task queues** receive tasks _and their related data_, run them, and deliver results. They support scheduling and are well-suited to computationally-intensive jobs in the background. Celery is the canonical Python example, with explicit scheduling support [3].

The line between them is fuzzy in products (RabbitMQ, Redis, Kafka, SQS each blur it), but the mental split is useful: a message queue carries "something happened, react to it" notifications; a task queue carries "run this job with this input" work items.

## Back pressure: protecting the system when the queue fills

Queues are buffers, but buffers are finite. If producers consistently outpace consumers, the queue grows without bound — and once it exceeds memory, performance collapses into cache misses and disk reads that make everything slower [2]. **Back pressure** is the answer: limit the queue size to maintain high throughput and good response times for the jobs already in flight. Once the queue is full, clients get a "server busy" or HTTP 503 and are told to try again later, ideally with **exponential backoff** so retries space out instead of hammering a recovering system [2].

Think of it as a pressure valve. A queue without back pressure is a pipe that bursts under sustained load — the queue itself becomes the bottleneck, the very thing it was meant to absorb. A queue with back pressure degrades gracefully: it serves the jobs it can at full speed and refuses new ones loudly, so producers can shed load instead of silently piling up latency.

## How I use this

The decision check is whether work on the request path takes longer than the request should hold open. If yes — anything that calls a slow third-party API, encodes media, fans out notifications, or runs a report — it goes onto a queue and the request returns immediately with a status handle. I default to a message queue for "react to this event" flows and a task queue for "run this expensive job" flows. And I always wire back pressure in from day one, because the failure mode of an unbounded queue under load is catastrophic and silent — by the time you notice, the queue is the outage.

## References

[1] "Patterns for microservices — sync vs async," Medium, 2022. [Online]. Available: [https://medium.com/inspiredbrilliance/patterns-for-microservices-e57a2d71ff9e](https://medium.com/inspiredbrilliance/patterns-for-microservices-e57a2d71ff9e)

[2] M. Thompson, "Applying back pressure when overloaded," Mechanical Sympathy blog, 2012. [Online]. Available: [http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html](http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html)

[3] Celery, "Celery — distributed task queue," 2024. [Online]. Available: [https://docs.celeryq.dev/en/stable/](https://docs.celeryq.dev/en/stable/)

[4] D. Martin, "Message queues," system-design-primer (open source), 2024. [Online]. Available: [https://github.com/donnemartin/system-design-primer#message-queues](https://github.com/donnemartin/system-design-primer#message-queues)

[5] "Little's law," Wikipedia. [Online]. Available: [https://en.wikipedia.org/wiki/Little%27s_law](https://en.wikipedia.org/wiki/Little%27s_law)

[6] Apache, "Apache Kafka," 2024. [Online]. Available: [https://kafka.apache.org/](https://kafka.apache.org/)

```quiz
Q: The primary purpose of putting a queue between a producer and a consumer is to…
- make the work itself run faster
- decouple their rates — the producer returns fast, the consumer works at its own pace
correct: 1
explain: The queue absorbs the rate mismatch. The work is not faster in absolute terms; it is just no longer blocking the producer.

Q: What is back pressure?
- speeding up consumers when the queue grows
- limiting the queue size and refusing new work (e.g. HTTP 503) when full, so the system degrades gracefully
correct: 1
explain: Back pressure caps the queue and tells producers to back off (often with exponential backoff on retry). Without it, an unbounded queue becomes the bottleneck.

Q: A task queue differs from a message queue in that it…
- carries tasks plus their input data and supports running expensive jobs with scheduling
- can only deliver one message at a time
correct: 0
explain: Task queues carry work items with their input and are built for computationally-intensive jobs, often with scheduling. Message queues carry "something happened" notifications.

Q: Why is an unbounded queue dangerous under sustained load?
- it never fills, so it is always safe
- it grows past memory, causing cache misses and disk reads that collapse throughput
correct: 1
explain: Without back pressure, the queue grows until it exceeds memory, at which point performance degrades badly. The queue itself becomes the outage.
```
