Publish/subscribe: why 2 ms jobs waited 31 seconds behind one slow service
Publish/subscribe vs direct calls in our simulation: with one 50 ms service among ten, fast jobs averaged 31,484 ms with direct calls and 1.87 ms with pub/sub.
In this lesson you will learn
- Why direct fan-out makes the publisher's time per event the sum of every subscriber's call
- How a topic with one subscription per consuming service lets each service work at its own speed
- Why pub/sub moves the slow consumer's backlog instead of removing it, and what to monitor
Builds on Lesson 60: Security monitoring: login anomaly detection 5 min read Next: A million likes at once

Drafted with AI assistance, then checked and edited by a human engineer · How we write
Short answer
When a publisher calls every subscriber in turn, its time per event is the sum of all the calls, so one slow subscriber sets the pace for everyone. In our virtual-time simulation (1,000 orders, one every 5 ms, 10 subscribers, one of them 50 ms slower) fast jobs averaged 31,484 ms with direct calls and 1.87 ms with pub/sub. The slow one still built a 902-event queue.
| Direct calls, 10 subscribers, one slow | Publisher time per event 67.97 ms against an order every 5 ms; peak publisher backlog 926 orders not yet started |
|---|---|
| Fast jobs, order placed to done | 31,483.88 ms average with direct calls; 1.87 ms with pub/sub (p99 62,339.77 ms vs 2.36 ms) |
| Pub/sub publisher | Assumed publish cost 0.07 ms per event at 10 subscribers; publisher backlog 0 |
| The slow subscriber | Still about 51 ms per event in both modes; its queue peaked at 902 with pub/sub and it finished at 51,511.67 ms instead of 67,960.95 ms |
| Without the slow one | Direct calls at 10 subscribers still took 18 ms per event and averaged 6,502.54 ms for fast jobs; pub/sub 1.87 ms |
| Setup | Our simulation (labs/sd-61, seed 61): virtual clock, hand-picked costs (0.3 ms call round trip, 1 to 2 ms work, +50 ms for one subscriber). Shape of the effect, not broker benchmarks |
If one service has to tell ten others that an order was placed, calling them one after another makes every order wait for the sum of all ten calls, so the slowest service sets the pace for everyone. Publish/subscribe lets each subscription process events independently, so the publisher can continue without waiting for subscriber work.
Prerequisites
A worker that gets a job every 5 ms and needs 18 ms per job has an offered load of ρ = λE[S] = 18/5 = 3.6, or 360% of one worker's capacity. The worker stays busy and its backlog grows while arrivals continue. The same queueing logic sits behind latency versus throughput.
Definitions
Publisher: the service that announces an event (here, "order placed") without knowing who will act on it.
Topic: a named channel the publisher sends each event to once. The broker makes the event available to every matching subscription.
Subscription: an independent delivery stream and backlog for a consuming service, which can share processing among multiple consumer instances. In Kafka, the equivalent position is tracked per partition within each consumer group.
Backlog: in our lab, the largest number of events that were placed but not yet started by a given party.
How publish/subscribe works
- The publisher sends the event to a topic and gets an acknowledgement from the broker. It does not wait for any subscriber.
- The broker makes the event available to each matching subscription; storage depends on the implementation. Google Cloud Pub/Sub delivers each message to every subscription attached to the topic (overview). RabbitMQ routes it through a fanout exchange to every bound queue, and in the tutorial a message sent before any queue is bound is discarded. Kafka keeps one log per partition, and each consumer group tracks its own offset in it (docs).
- Each subscriber pulls or receives from its own subscription at its own speed and acknowledges what it finished.

With direct calls the publisher pays for every subscriber on every event. With a topic it pays for one publish, and the broker does the fan-out. Our lab assumes that publish costs 0.05 ms plus 0.002 ms per subscription, so 0.052 ms at one subscriber and 0.07 ms at ten.
Why direct calls pile up: the arithmetic
With direct calls in sequence, the publisher's service time per event is the sum over subscribers of round trip plus work:
S_direct = sum over i of (rtt + work_i)
= 10 x 0.3 ms + nine fast jobs (1 to 2 ms) + one slow job (about 51 ms)
= 67.97 ms per event (lab output, seed 61)
arrivals every 5 ms -> offered load ρ = 67.97 / 5 = 13.6
At 13.6 the publisher's queue grows by about 12.6 events for every one it finishes. A fast subscriber receives event n only after the publisher reaches it, so it inherits the slow one's delay. Even without the slow subscriber, ten fast calls took 18 ms per event, an offered load of 3.6, and fast jobs averaged 6,502.54 ms.
With pub/sub the publisher's service time is the publish cost alone, 0.07 ms against a 5 ms gap, an offered load of about 0.014, so it never builds a backlog. Each fast subscriber is busy about 30% of the time (1 to 2 ms of work every 5 ms), which keeps its wait close to its own work time.
Worked example

1,000 orders, one every 5 ms -> the last one arrives at 4,995 ms
direct: publisher starts one order every 67.97 ms (average; seeded work varies per event)
by 4,995 ms it has started floor(4,995 / 67.97) + 1 = 74 orders
1,000 - 74 = 926 orders placed but not started (lab: 926)
order n waits about n x (67.97 - 5) ms before its calls start
average over n = 0..999: about 499.5 x 62.97 = 31,453 ms,
plus its place in the call sequence (lab: 31,483.88 ms)
pub/sub: fast completion ≈ 0.07 ms publish + 0.3 ms hand-off + 1.5 ms mean work = 1.87 ms
slow subscriber: first start at 0.37 ms, then about 51.5 ms per event,
so by 4,995 ms it has started about (4,995 - 0.37) / 51.5 + 1 ≈ 98 events
(98 in the seeded run); 1,000 - 98 = 902 waiting (lab: 902)
Assumed by hand: round trip 0.3 ms, work 1 to 2 ms, the +50 ms on subscriber six, the hand-off 0.3 ms and the publish cost. Derived by the simulation: every backlog, wait and finish time.
Code
cd labs/sd-61
node run.mjs # Node.js 22, no packages, no network, under a second on a virtual clock
Compare the two 10 yes rows: pub ms/event 67.97 against 0.07, fast avg ms 31483.88 against 1.87, and the slow subscriber's backlog, 926 against 902. The slow subscriber finished 16,449.28 ms earlier with pub/sub, because its work overlapped with the other subscribers instead of waiting behind their calls.

Failure modes and edge cases
- The slow consumer's backlog grows. Pub/sub moved the 50 ms problem into the invoice service's own queue. Watch each subscription's oldest unacknowledged message age and message count, and add consumers where messages can be processed independently (the next part, competing consumers); a single ordered key remains limited by its serial processing rate. Or make the work cheaper.
- Isolation depends on broker capacity: growing queues can exhaust shared resources and throttle publishing. RabbitMQ, for example, blocks publishers when its memory alarm fires. Monitor broker resource alarms alongside subscription backlog.
- Check retention and overflow policies. Configured expiry (RabbitMQ TTL, for example) or storage limits can remove unread events; alert before the applicable limit.
- Redelivery. With RabbitMQ manual acknowledgements, closing a channel or connection requeues its unacknowledged deliveries; another consumer may receive them. Durability and acknowledgement mode are separate settings (RabbitMQ).
- Ordering. Publish related events in causal order and configure the broker's ordering guarantee. In Google Pub/Sub, use the same ordering key and region, and enable subscription ordering (docs). Preserve that order when processing messages concurrently.
- Hidden coupling. If checkout needs the invoice service's answer before replying to the user, it is a request, and that call should stay direct.
Trade-offs
| Option | Good for | Costs you | Use when |
|---|---|---|---|
| Direct calls in sequence | Simple code, immediate errors | Publisher time is the sum of every call | One or two fast, critical callees |
| Direct calls in parallel | Lower latency than sequence | Still waits for the slowest; failure handling per callee | The caller truly needs every answer now (our lab did not test this) |
| Publish/subscribe | Many independent reactions to one event | A broker to run, redelivery, backlog and broker resources to monitor | Subscribers can act later and at their own speed |
When you split a monolith into services, this choice comes up early. Services that keep direct calls find each other through service discovery.
Common mistakes
In an interview
Ask whether the caller needs each service's answer before it replies. If not, publish once to a topic with one subscription per service; if it does, keep that call direct.
Practice
BeginnerWhat does a topic do that a queue does not?
It makes each message available to every subscription; a queue gives each message to one consumer.
IntermediateDirect calls take 18 ms per event and events arrive every 5 ms. What happens?
Offered load is 3.6, so the publisher's backlog grows by about 2.6 events per event served while arrivals continue.
IntermediateWhy did fast jobs wait 31 s with direct calls?
They queued behind earlier orders that each took 67.97 ms.
AdvancedWhy did the slow subscriber finish earlier with pub/sub?
Only its own work limited it.
AdvancedHow can a slow subscriber still slow the publisher?
Its backlog fills shared broker memory or disk, and the broker throttles publishing.
AdvancedWhen should you keep a direct call?
When the caller cannot answer its own user without the callee's result.
Cheat sheet
| Term or formula | Meaning |
|---|---|
| S_direct = sum (rtt + work_i) | Publisher time per event with sequential calls |
| offered load ρ = λE[S] = S / arrival gap | Above 1, the backlog grows while arrivals continue |
| Topic | Publish once; every matching subscription can read the event |
| Subscription | An independent delivery stream and backlog for a consuming service, which can share processing among multiple consumer instances |
| At-least-once | Duplicates possible; dedupe in the same transaction |
| Oldest message age | The per-subscription backlog alert |
Further reading
- Google Cloud Pub/Sub overview and ordering guide: subscriptions, ordering keys and their requirements.
- Apache Kafka documentation: logs, partitions and consumer groups with their own offsets.
- RabbitMQ Publish/Subscribe tutorial, plus the memory alarm and acknowledgement pages.
This article describes general messaging patterns and public documentation; it does not describe any company's private systems, and the diagrams are conceptual. Every number comes from labs/sd-61 (seed 61), a virtual-time simulation with hand-picked costs. It shows the shape of the effect, not broker benchmarks.
Frequently asked questions
Does publish/subscribe make a slow consumer faster?
No. In our simulation the slow subscriber still needed about 51 ms per event in both modes. The publisher and the other subscribers stopped waiting for it, while its own queue grew to 902 events.
What is the difference between a topic and a queue?
A queue hands each message to one consumer. A topic makes each message available to every subscription, and each subscription has its own delivery stream and backlog, which several instances of one consuming service can share.
Can one slow subscriber still affect the others?
Yes, through the broker. Its growing queue uses shared memory and disk, and brokers such as RabbitMQ block publishers when a resource alarm fires. Watch broker resources as well as each backlog.
Sources
- Pub/Sub overview, Google Cloud documentationcloud.google.com
- Ordering messages, Google Cloud Pub/Sub documentationdocs.cloud.google.com
- Apache Kafka 4.1 documentation: introductionkafka.apache.org
- RabbitMQ tutorial: Publish/Subscriberabbitmq.com
- RabbitMQ: memory threshold and limitrabbitmq.com
- RabbitMQ: time-to-live and expirationrabbitmq.com
- RabbitMQ: consumer acknowledgements and publisher confirmsrabbitmq.com