Skip to content

Scaling FIFO Queues

Scaling FIFO Queues

FIFO queues work with this package, and depth, pickup time and scaling all behave normally. But they change what a worker count means, in a way that will quietly waste money if it goes unnoticed.

A standard queue hands a message to whoever asks, so ten workers give ten jobs running at once. A FIFO queue delivers at most one message per message group at a time. The next message in a group is not released until the previous one is deleted or its visibility timeout expires. Parallelism is therefore capped by the number of distinct message groups in the backlog — not by the number of workers polling.

Point ten workers at a FIFO queue whose jobs all share one message group and you get one worker doing work and nine polling an empty response, indefinitely.

What decides the group

Laravel derives MessageGroupId from the job:

class ScrapeStudent implements ShouldQueue
{
    public function __construct(
        public int $tenantId,
        public int $studentId,
    ) {}

    public function messageGroup(): string
    {
        return "tenant-{$this->tenantId}";
    }
}

A job dispatched to a .fifo queue without messageGroup() is rejected by AWS at dispatch time, not at processing time — the exception surfaces where the job is queued.

Choosing a group is choosing your concurrency

The group is the ordering boundary and the concurrency boundary at once, so the decision is the same decision:

messageGroup() returns Ordering guaranteed across Max concurrent jobs
A constant Everything on the queue 1
The tenant That tenant's jobs One per tenant
Tenant plus a shard Jobs within a shard Shards per tenant
The individual record Nothing meaningful Effectively unbounded

For the common case of a per-tenant limit on some downstream system — an API that accepts five concurrent callers — the tenant alone is too coarse. It gives ordering you probably do not need and a concurrency of one. Sharding restores the parallelism while keeping the cap exact:

public function messageGroup(): string
{
    return "tenant-{$this->tenantId}-".($this->studentId % 5);
}

Five groups per tenant, so at most five of that tenant's jobs run at once — enforced by the queue itself, independently of how many workers exist or how many hosts they run on.

Setting the worker count

With the group count fixed, the worker count should match it rather than exceed it. Workers beyond the number of groups cost memory and polling and can never receive a job.

'queues' => [
    'scrape-*.fifo' => [
        'profile' => ConnectionLimitedProfile::class,
        'workers' => ['max' => 5],
    ],
],

Glob keys matter more than usual here: every FIFO queue name ends in .fifo by AWS requirement, so one pattern can cover a whole family of them.

ConnectionLimitedProfile suits this shape — workers.min of zero so an idle queue costs nothing, and a hard workers.max treated as a fleet-wide cap in cluster mode rather than a per-host one.

Things that bite

Deduplication can silently drop work. FIFO queues reject a message whose MessageDeduplicationId matches one seen in the last five minutes. With content-based deduplication enabled, two genuinely distinct jobs with identical payloads become one. Give jobs an explicit deduplicationId() when their payloads may repeat.

Delays are not available. DelaySeconds is rejected on FIFO queues, so ->delay() does not work. Laravel omits it rather than failing, which means a delayed dispatch runs immediately.

A stuck job blocks its whole group. Because ordering is guaranteed, a message that keeps timing out holds up every message behind it in that group until it exhausts its attempts. On a standard queue that job would be one slow item among many; here it is a stopped line. Keep workers.timeout_seconds — the per-job limit — tight enough that a hung job fails rather than lingering, and watch the failure fuse — a FIFO backlog that stops draining looks exactly like the downstream outage the fuse exists to catch.

Throughput has a ceiling. A FIFO queue handles far fewer messages per second than a standard one unless high-throughput mode is enabled, and that mode distributes by message group — another reason a single group is the wrong choice at volume.

Verifying

php artisan queue:autoscale:doctor

Flags any .fifo queue configured for more than one worker, with the reminder that the parallelism is only real if the backlog spans that many message groups. See Check Your Configuration.

The package's own FIFO behaviour is covered by integration specs that run against ElasticMQ. See Requirements for how to start it.