Skip to content

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.