Scale a subscription with concurrency¶
{
Name: "transcoder",
Pattern: "media.>",
Shape: messaging.Fanout,
Concurrency: 4,
Handler: handleTranscode,
}
Concurrency is how many goroutines read this subscription's deliveries. Zero, the default, means
one. There is no worker pool, no second queue, and nothing that scales itself at runtime: the
readers share the queue the backend already owns, and the number is a decision you make at wiring
time or when you call Subscribe, not a knob the bus turns for you while it runs.
It costs ordering, and it costs it the moment you set it above one¶
Concurrency: 1 keeps arrival order. Anything above it does not.
N goroutines draining one queue concurrently is what makes concurrency useful and what takes
ordering away. That is decided here, at wiring, rather than discovered later when a queue happens
to be deep: a subscriber that needs ordering says so by leaving Concurrency at its default, and
this module will not silently take it away under load, because there is no load-driven scaling to
take it away with.
The conformance suite holds this exactly: a Concurrency: 1 subscription is asserted to preserve
order, and a subscription above one is asserted only to deliver everything. The absence of an
ordering assertion there is deliberate, so nobody adds one later and makes an unordered guarantee
load-bearing.
What it does not buy¶
A wedged handler still wedges its own consumer, not the readers underneath it as separate units. Concurrency is not a resilience strategy for a handler that never returns: a reader that calls a handler which ignores its context blocks that reader for good, and Go cannot end a goroutine to reclaim it. Handing the same subscription three more readers gives you three more readers doing useful work alongside the wedged one, not a mechanism that notices and works around it.
What isolates a subscription from the rest of the bus is not concurrency: every subscription is its
own supervised unit. A subscription whose readers all end in failure is
quarantined and, depending on its RestartPolicy, restarted; a sibling subscription's readers are
unaffected either way. See wire a bus into a controller for what that looks like
from an operator's side, and hunt for handler panics for finding the
defect before a broker delivers it.
When to reach for it¶
Taking a bigger share of a burst is not one of the cases, which is the mistake people make
first. A queue group distributes per subscription, not per worker: a competing-consumer member
with Concurrency: 4 still receives one member's share of the group, faster, not a bigger share of
it. See subscribe at runtime for the mechanism that does change a share:
adding another subscription.
What concurrency buys is throughput on a subscription that already receives everything it is going to receive: a handler doing meaningful work per message, on a subject where ordering across messages does not matter to you, benefits from more than one reader draining the same queue at once.
Deciding the number¶
There is no scaling threshold to tune, because there is no scaling. Pick a number that matches the work: a handler bound on a downstream call benefits from enough readers to keep several calls in flight; a handler bound on CPU rarely benefits from more readers than the host has cores to run them on. Measure your own handler under your own load rather than assuming a number that suited a different handler.