JSTGTECH
← Back to blog

Service spotlight: Amazon SQS queue depth, DLQs, and visibility

3 min read

SQS is old, boring, and one of the most reliable services AWS runs — which is exactly why it’s easy to wire up carelessly and not notice until a queue backs up in production. The service itself rarely fails; the mistakes are almost always in how visibility timeouts, DLQs, and consumer scaling are configured around it.

The core model, briefly

A producer sends a message; a consumer polls, receives it, processes it, and explicitly deletes it. Between receive and delete, the message is invisible to other consumers for the visibility timeout duration — not deleted, just hidden, so a second consumer doesn’t pick up the same message while the first is still working it. If the consumer crashes, times out, or never calls delete, the message reappears after the timeout expires and gets redelivered. That reappear-on-failure behavior is the entire reliability model, and most SQS production issues trace back to a mismatch somewhere in that loop.

Mistake 1: visibility timeout shorter than processing time

If your consumer takes 90 seconds to process a message but the queue’s visibility timeout is set to the default 30 seconds, the message becomes visible again while it’s still being processed — a second consumer picks it up, and now you’re processing the same message twice concurrently. For anything non-idempotent (charging a card, sending an email, decrementing inventory), that’s a correctness bug, not just wasted compute. Set the visibility timeout to comfortably exceed your maximum expected processing time, not the average — or use ChangeMessageVisibility to extend it dynamically from inside a long-running handler if processing time varies widely.

Mistake 2: no DLQ, or a DLQ nobody watches

A dead-letter queue catches messages that fail processing repeatedly (by maxReceiveCount) instead of retrying forever and blocking the queue behind one poison message. Skipping a DLQ means a single malformed message can loop indefinitely, burning consumer capacity on retries that will never succeed. But the more common failure is having a DLQ and never alarming on it — messages quietly pile up, nobody notices for weeks, and by the time someone checks, the redrive window (or business relevance of the messages) has passed. A DLQ without a CloudWatch alarm on ApproximateNumberOfMessages Visible is a DLQ that isn’t doing its job.

Mistake 3: treating queue depth as the only signal

ApproximateNumberOfMessagesVisible tells you how many messages are waiting, but on its own it doesn’t tell you whether that’s a processing outage or just a traffic spike your consumers will burn down in ten minutes. Pair it with ApproximateAgeOfOldestMessage — a queue that’s deep but where the oldest message is only 90 seconds old is healthy and scaling; a queue where the oldest message is 40 minutes old means something downstream is actually stuck, regardless of current depth. Age-of-oldest is usually the better alarm trigger for “something is broken” versus “we’re busy.”

Standard vs FIFO

Standard queues are at-least-once delivery with best-effort ordering and effectively unlimited throughput — the right default for most workloads, and your consumer logic needs to be idempotent regardless (duplicate delivery is a normal, expected occurrence, not an edge case). FIFO queues add exactly-once processing and strict ordering **within a message group**, at the cost of a throughput ceiling (3,000 messages/sec with batching, per API action) and higher per-request cost. Reach for FIFO only when ordering is a genuine correctness requirement (e.g., applying account balance changes in sequence) — defaulting to FIFO “to be safe” trades away throughput headroom for a guarantee most workloads don’t actually need.

A practical tip

Set a redrive policy that moves messages back from the DLQ to the source queue (SQS supports this natively now, no custom script needed) as part of your incident-response runbook, not as something you improvise during an outage. Deciding in advance how many retries a message deserves before landing in the DLQ, and what “redrive after the bug is fixed” actually looks like operationally, is a five-minute conversation before go-live and a much worse one at 2am with a few thousand stuck messages.

Related posts