DEV Community

Cover image for SQS Fair Queues: one loud customer no longer ruins your day
OhadTutay
OhadTutay

Posted on

SQS Fair Queues: one loud customer no longer ruins your day

If you run a SaaS on AWS, chances are all your customers share one SQS queue. It's far less to manage than a queue per tenant. Until one customer dumps 200k messages at 9am and everyone else's jobs sit there for twenty minutes.

That's the noisy neighbor problem. AWS shipped fair queues in July 2025 to deal with it.

What goes wrong today

A standard queue doesn't care who sent a message. Ordering is best-effort, so under a backlog your consumers just chew through whatever's in front of them, and whoever sent the most wins. Tenant A floods the queue, and B, C and D wait behind the pile. Their dwell time, the gap between a message arriving and getting processed, goes up because of traffic that isn't theirs.

QUEUE (backlog)                  CONSUMERS
[A A A A A B A A C A A D]  ───►  [A][A][A][A][A][A][A][B]
                                 everyone waits, not just A
Enter fullscreen mode Exit fullscreen mode

You can fix this the ugly way. Over-provision consumers so bursts get absorbed, or split into a queue per tenant. Per-tenant queues aren't expensive in themselves, since SQS bills per request rather than per queue, but you end up alarming on and long-polling 400 mostly-idle queues.

What fair queues do

SQS watches how in-flight messages are spread across tenants. When one takes too big a share it gets labelled noisy, and other tenants' messages start going out first.

QUEUE                            CONSUMERS
[A A A A A B A A C A A D]  ───►  [B][C][D][A][A][A][A][A]
 A is noisy → B, C, D go first   B, C, D stay fast. A waits a bit.
Enter fullscreen mode Exit fullscreen mode

A isn't throttled and nothing gets dropped. It goes to the back of the line until its backlog clears, and it still fills whatever capacity nobody else is using, which is why it keeps five slots above. Your total throughput stays where it was.

Adding a group ID

Set a MessageGroupId when you send. That's it. No new queue type, and nothing changes on the consumer side.

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

await sqs.send(
  new SendMessageCommand({
    QueueUrl: queueUrl,
    MessageBody: body,
    MessageGroupId: "tenant-123", // who this message belongs to
  })
);
Enter fullscreen mode Exit fullscreen mode

Pick a meaningful group ID - like a customer ID or request type, not a per-request ID. SQS detection requires 30 in-flight messages per group to trigger, so hyper-granular IDs will just silently fail.
Also, instrument all your producers at once. Any message missing a group ID gets treated as a unique tenant, meaning a partial rollout will flood SQS with thousands of single-message "tenants" and throw off its metrics.

One trap: on a standard queue MessageGroupId is only a label. It does not give you FIFO ordering, even though it's the same property name. Messages sharing a group id still run in parallel, which is what you want here, but it catches people coming from FIFO.

How it picks the noisy one

Two signals, per the docs. A tenant gets flagged when it holds more than 10% of in-flight messages and has at least 30 of its own in flight. It also gets flagged when its recent share of consumer processing time goes over 10%, which is the one that catches a tenant with few messages that each take forever.

Treat both numbers as approximate. AWS says so itself, so don't write a load test that expects the switch to flip at message 30.

A tenant goes quiet again once its backlog is consumed, or after five continuous minutes with nothing in flight. If several are noisy at once, whichever has the fewest in-flight messages gets served first.

Size your consumer fleet before you test any of this. None of it works unless enough messages are in flight for one tenant's share to stand out, and people trying it with two or three consumers usually can't trigger it at all, then conclude the feature is broken. On Lambda your in-flight count is concurrency times batch size.

Checking it actually works

SQS emits five fair-queue metrics: ApproximateNumberOfNoisyGroups, the Visible / NotVisible / Delayed variants of ApproximateNumberOfMessages...InQuietGroups, and ApproximateAgeOfOldestMessageInQuietGroups.

Graph ApproximateNumberOfMessagesVisible against ApproximateNumberOfMessagesVisibleInQuietGroups. During a burst the first spikes and the second stays flat. That flat line is the whole feature in one picture.

Finding out who's being loud takes a bit more. Contributor Insights will rank tenants for you without blowing up your custom metric bill, but it builds that ranking from your application logs, so your consumer has to log the MessageGroupId itself.

The catch

It isn't free. Any send, receive, delete or visibility change where at least one message carries a group ID gets billed at fair queue rates on top of standard rates, roughly ten cents per million requests in US regions. Check the pricing page for yours. Watch the "at least one" there: a batch of ten where a single message has a group ID makes the whole request a fair queue request.

There are decent reasons to skip it. If your queue rarely builds a backlog, a burst from one tenant isn't hurting anybody. If your consumers don't run wide enough, detection won't fire at all. And if nothing in your product cares how long a job waits, you're paying a surcharge for a property nobody will notice. It pays off when you're high throughput, multi-tenant, and latency is something you've promised customers.

AWS has a sample app with a load generator and a CloudWatch dashboard if you want to watch it happen before trusting it in prod. Deploy with CDK, run the Artillery test, then cdk destroy before your bill notices.

Top comments (0)