Problem
route.Commit issues one DeleteMessage call per consumed message. Since ReceiveMessage already runs at MaxNumberOfMessages=10, a full receive cycle costs 1 receive call and up to 10 delete calls, so deletes dominate the API request count — and SQS bills per request regardless of how many messages a request carries.
At 1M messages/day that is ~1M delete requests where DeleteMessageBatch (up to 10 entries per call, the same ceiling as MaxNumberOfMessages) would need ~100k.
Why a plain time-based buffer is not enough
The obvious fix — accumulate deletes and flush on a timer — trades cost for latency, and on FIFO queues that trade is a bad one: SQS does not deliver the next message of a MessageGroupId while one is still in flight, so every millisecond a delete sits in a buffer throttles the whole group.
Any batching design here has to avoid paying that latency in the common case.
Proposal
Make the end of the receive cycle the primary flush trigger instead of a timer. A per-route in-flight counter is incremented in GetMessages and decremented as each message resolves (committed, backed off, or failed in the handler); reaching zero flushes immediately. A linger stays as the safety net for when a slow handler holds the batch back.
Measured against LocalStack:
| scenario |
messages |
delete API calls |
wall time |
| standard, today |
100 |
100 DeleteMessage |
— |
| standard, batched |
100 |
10 DeleteMessageBatch |
— |
| FIFO, 4 groups, today |
60 |
60 DeleteMessage |
528ms |
| FIFO, 4 groups, batched |
60 |
6 DeleteMessageBatch |
322ms |
FIFO gets faster rather than slower, because workers stop blocking on a delete round trip.
Notes
- This is a breaking change to the
SQSClient interface (DeleteMessageBatch is added). The AWS SDK client already satisfies it; only custom implementations are affected.
- It should be opt-in, so existing consumers keep the current behaviour on upgrade.
ChangeMessageVisibility has the same batching opportunity, but it is a smaller win: every message goes through Commit, while only a fraction is ever extended. Worth a separate issue.
I have an implementation ready and will open a PR linked to this issue.
Problem
route.Commitissues oneDeleteMessagecall per consumed message. SinceReceiveMessagealready runs atMaxNumberOfMessages=10, a full receive cycle costs 1 receive call and up to 10 delete calls, so deletes dominate the API request count — and SQS bills per request regardless of how many messages a request carries.At 1M messages/day that is ~1M delete requests where
DeleteMessageBatch(up to 10 entries per call, the same ceiling asMaxNumberOfMessages) would need ~100k.Why a plain time-based buffer is not enough
The obvious fix — accumulate deletes and flush on a timer — trades cost for latency, and on FIFO queues that trade is a bad one: SQS does not deliver the next message of a
MessageGroupIdwhile one is still in flight, so every millisecond a delete sits in a buffer throttles the whole group.Any batching design here has to avoid paying that latency in the common case.
Proposal
Make the end of the receive cycle the primary flush trigger instead of a timer. A per-route in-flight counter is incremented in
GetMessagesand decremented as each message resolves (committed, backed off, or failed in the handler); reaching zero flushes immediately. A linger stays as the safety net for when a slow handler holds the batch back.Measured against LocalStack:
DeleteMessageDeleteMessageBatchDeleteMessageDeleteMessageBatchFIFO gets faster rather than slower, because workers stop blocking on a delete round trip.
Notes
SQSClientinterface (DeleteMessageBatchis added). The AWS SDK client already satisfies it; only custom implementations are affected.ChangeMessageVisibilityhas the same batching opportunity, but it is a smaller win: every message goes throughCommit, while only a fraction is ever extended. Worth a separate issue.I have an implementation ready and will open a PR linked to this issue.