SQS Error: Visibility Timeout Too Short Causes Duplicate Message Processing
Fix SQS messages processed twice when the visibility timeout expires before the consumer finishes: diagnose redelivery, slow handlers, and idempotency.
- #automation
- #devops
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
Amazon SQS hides a received message for the queue’s visibility timeout. If the consumer does not delete the message before that window expires, SQS makes it visible again and another consumer receives it — so a slow handler causes the same message to be processed more than once. There is no exception; the symptom is duplicated side effects and redelivery, visible in logs and metrics:
[worker-1] processing message id=b1f0... (attempt 1)
[worker-1] still working... (elapsed 34s)
[worker-2] processing message id=b1f0... (attempt 2) <- redelivered while worker-1 runs
CloudWatch confirms the redelivery pattern:
ApproximateReceiveCount > 1 for messages that eventually succeed
NumberOfMessagesReceived >> NumberOfMessagesDeleted
Symptoms
- The same message ID is processed by two workers, or by the same worker twice.
- Downstream duplicate effects: double charges, double emails, repeated writes.
ApproximateReceiveCountclimbs above 1 for messages that are not actually failing.NumberOfMessagesReceivedgreatly exceedsNumberOfMessagesDeleted.- Messages sometimes land in the DLQ after
maxReceiveCountdespite eventually completing.
Common Root Causes
- Visibility timeout shorter than processing time — the handler routinely takes longer than the timeout, so the message reappears mid-processing.
- Occasional slow messages — most messages are fast but heavy ones exceed a timeout sized for the average.
- Delete after slow post-processing — the message is deleted only after a long downstream call that sometimes overruns the window.
- No visibility extension for long jobs — a legitimately long task does not extend visibility as it runs.
- Consumer stall/GC pause — the worker pauses long enough for the timeout to lapse.
- Non-idempotent handler — redelivery is inevitable at-least-once, but the handler treats each receive as unique.
Diagnostic Workflow
Read the queue’s configured visibility timeout:
aws sqs get-queue-attributes --queue-url <url> \
--attribute-names VisibilityTimeout ApproximateNumberOfMessagesNotVisible
Measure actual handler duration against that timeout (from your logs):
grep -E 'processing message|completed message' worker.log \
| awk '{print $0}' | tail -40
# compare elapsed-per-message to VisibilityTimeout
Check receive counts to confirm redelivery vs. genuine failure:
aws sqs receive-message --queue-url <url> \
--attribute-names ApproximateReceiveCount --max-number-of-messages 10 \
--query 'Messages[].{Id:MessageId,Recv:Attributes.ApproximateReceiveCount}'
Compare received vs. deleted over time in CloudWatch:
aws cloudwatch get-metric-statistics --namespace AWS/SQS \
--metric-name NumberOfMessagesDeleted --dimensions Name=QueueName,Value=<q> \
--period 300 --statistics Sum --start-time <t0> --end-time <t1>
If using a Lambda trigger, confirm the function timeout vs. the queue visibility (visibility should be >= 6x function timeout):
aws lambda get-function-configuration --function-name <fn> --query 'Timeout'
Example Root Cause Analysis
An order-fulfilment queue was double-shipping a small fraction of orders. Logs showed the same message ID handled by two workers seconds apart, and ApproximateReceiveCount was 2 for the affected messages — yet they were not in the DLQ, so nothing was “failing.” The queue’s VisibilityTimeout was 30 seconds. Handler timing showed most messages finished in ~8 seconds, but orders with many line items took 35-45 seconds.
Those heavy messages exceeded the 30-second window, so SQS made them visible again while worker-1 was still processing, and worker-2 picked them up and shipped a second time. The visibility timeout had been sized for the average, not the tail.
Two fixes were applied. First, the handler was made idempotent using the order ID as an idempotency key so a redelivered message is recognized and its side effect applied once — the durable safety net, since at-least-once delivery guarantees duplicates eventually. Second, for genuinely long messages the worker was updated to extend visibility with a heartbeat (ChangeMessageVisibility) as it works, and the base VisibilityTimeout was raised to comfortably exceed the observed tail. After both changes, redeliveries stopped causing duplicate shipments even when a message was occasionally re-received.
Prevention Best Practices
- Set the visibility timeout above the tail (p99+) of handler duration, not the average, with headroom.
- Make consumers idempotent on a business key — at-least-once delivery guarantees duplicates, so idempotency is the real fix, not just a longer timeout.
- Extend visibility with
ChangeMessageVisibility(a heartbeat) for legitimately long-running messages instead of relying on one large fixed timeout. - For Lambda-triggered queues, set
VisibilityTimeoutto at least 6x the function timeout, per AWS guidance. - Delete the message immediately after the work’s effect is durable, not after unrelated slow post-processing.
- Alarm when
ApproximateReceiveCountor the received-vs-deleted gap rises, to catch redelivery before it duplicates effects.
Quick Command Reference
# Read the visibility timeout
aws sqs get-queue-attributes --queue-url <url> \
--attribute-names VisibilityTimeout
# Check receive counts (redelivery signal)
aws sqs receive-message --queue-url <url> \
--attribute-names ApproximateReceiveCount \
--query 'Messages[].Attributes.ApproximateReceiveCount'
# Extend visibility for a long job (heartbeat)
aws sqs change-message-visibility --queue-url <url> \
--receipt-handle <rh> --visibility-timeout 120
# Lambda timeout vs. queue visibility (want visibility >= 6x)
aws lambda get-function-configuration --function-name <fn> --query 'Timeout'
Conclusion
SQS double-processing is not a delivery bug — it is a visibility timeout sized shorter than the work, letting SQS redeliver a message that is still being processed. Size the timeout to the slow tail, and extend it with a heartbeat for genuinely long jobs. But the real fix is idempotency: SQS is at-least-once by design, so duplicates will happen eventually no matter how you tune the timeout, and only an idempotent handler keyed on a business ID guarantees each message’s effect is applied exactly once.
Fixed it? Get 500 Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.