Guide · AWS SQS
SQS Batch Operations for MCP Servers — SendMessageBatch, Lambda ESM batchSize, Partial Failures
SQS batch operations allow you to send, receive, and delete up to 10 messages in a single API call, reducing both cost and latency compared to one-at-a-time operations. For MCP monitoring pipelines where a probe cycle generates dozens of results — one per monitored endpoint — batching is the primary mechanism to keep SQS costs linear with message volume rather than proportional to API call count. The three operations are SendMessageBatch (send up to 10, max 256 KB total body), ReceiveMessage with MaxNumberOfMessages (receive up to 10 per call), and DeleteMessageBatch (delete up to 10 by receipt handle). Lambda's Event Source Mapping adds two additional levers: batchSize (how many messages Lambda receives per invocation, up to 10,000 for standard queues) and maximumBatchingWindowInSeconds (how long Lambda waits to accumulate a full batch before invoking). The critical operational detail: partial batch failure — when one message in a batch fails processing, what happens to the rest.
TL;DR
SendMessageBatch: up to 10 messages, 256 KB total. ReceiveMessage: up to 10 per call (set MaxNumberOfMessages: 10 always). DeleteMessageBatch: up to 10 receipt handles per call — always batch your deletes. Lambda ESM batchSize: 1–10 for FIFO queues, 1–10,000 for standard queues. Enable reportBatchItemFailures in the Lambda response to retry only failed items in a batch instead of retrying the entire batch — without it, any single failure retries all 10 messages.
SendMessageBatch: cost and throughput
SQS charges per 64 KB chunk of payload per API call. A single SendMessage with a 1 KB body counts as one request. A SendMessageBatch with 10 × 1 KB bodies also counts as one request. Sending 10 probe results individually costs 10× what sending them as a batch costs.
Batch limits:
- Maximum 10 messages per
SendMessageBatchcall. - Maximum 256 KB total body size across all messages in the batch.
- Each message in the batch must have a unique
Idfield (1–80 alphanumeric characters) — used to map errors in the response back to individual messages. - For FIFO queues, each message in the batch must include
MessageGroupIdandMessageDeduplicationId(if content-based deduplication is off).
import { SQSClient, SendMessageBatchCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
// Send a batch of probe results
async function sendProbeResultBatch(
queueUrl: string,
results: ProbeResult[]
): Promise {
if (results.length === 0) return;
// SQS batch max is 10
for (let i = 0; i < results.length; i += 10) {
const chunk = results.slice(i, i + 10);
const resp = await sqs.send(new SendMessageBatchCommand({
QueueUrl: queueUrl,
Entries: chunk.map((result, idx) => ({
Id: `msg-${idx}`, // unique within batch
MessageBody: JSON.stringify(result),
MessageGroupId: result.endpointId, // FIFO: per-endpoint ordering
MessageDeduplicationId: `${result.endpointId}-${result.probeRunId}`,
})),
}));
if (resp.Failed && resp.Failed.length > 0) {
for (const failure of resp.Failed) {
console.error('SendMessageBatch partial failure', {
id: failure.Id,
code: failure.Code,
message: failure.Message,
senderFault: failure.SenderFault,
});
// SenderFault=true: your message is malformed (not retriable as-is)
// SenderFault=false: service-side error (retriable)
}
}
}
}
ReceiveMessage and DeleteMessageBatch
Always set MaxNumberOfMessages: 10 on ReceiveMessage calls. The default is 1 — using the default means each call returns at most one message, regardless of how many are available, which is ten times less efficient than it could be.
After processing, batch your deletes. SQS charges one API call per DeleteMessage and one call per DeleteMessageBatch regardless of how many handles are in the batch. Deleting 10 messages individually costs 10×.
import { SQSClient, ReceiveMessageCommand, DeleteMessageBatchCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
async function drainQueue(queueUrl: string): Promise {
const resp = await sqs.send(new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10, // always 10 — leave nothing on the table
WaitTimeSeconds: 20, // long polling
AttributeNames: ['All'],
MessageAttributeNames: ['All'],
}));
if (!resp.Messages || resp.Messages.length === 0) return;
// Process all messages
const deleteEntries: { Id: string; ReceiptHandle: string }[] = [];
for (const msg of resp.Messages) {
try {
await processProbeResult(JSON.parse(msg.Body!));
deleteEntries.push({ Id: msg.MessageId!, ReceiptHandle: msg.ReceiptHandle! });
} catch (err) {
console.error('Failed to process message', msg.MessageId, err);
// Do not add to deleteEntries — message will reappear after visibility timeout
}
}
if (deleteEntries.length > 0) {
await sqs.send(new DeleteMessageBatchCommand({
QueueUrl: queueUrl,
Entries: deleteEntries,
}));
}
}
Lambda ESM: batchSize and maximumBatchingWindowInSeconds
Lambda's Event Source Mapping has two batch controls that interact:
| Parameter | Standard SQS | FIFO SQS | Effect |
|---|---|---|---|
batchSize | 1–10,000 | 1–10 | Maximum messages per Lambda invocation |
maximumBatchingWindowInSeconds | 0–300 | 0–300 (but FIFO ordering constraint applies) | How long Lambda waits to accumulate a full batch before invoking |
With maximumBatchingWindowInSeconds=0 (default), Lambda invokes the function as soon as messages arrive, up to batchSize. A burst of 100 messages at once with batchSize=10 triggers 10 Lambda invocations simultaneously — each with up to 10 messages.
With maximumBatchingWindowInSeconds=30 and batchSize=10, Lambda waits up to 30 seconds or until it has 10 messages, then invokes once. This reduces invocation count but increases latency. For probe-result pipelines where low latency matters, keep the batching window at 0.
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
probeFunction.addEventSource(new SqsEventSource(probeQueue, {
batchSize: 10,
maxBatchingWindow: Duration.seconds(0), // invoke immediately when messages arrive
reportBatchItemFailures: true, // enable partial batch failure response
}));
Partial batch failure: reportBatchItemFailures
By default, when a Lambda function throws an unhandled exception while processing an SQS batch, the entire batch is considered failed. All 10 messages become visible again and are reprocessed — including the 9 that succeeded. This can cause duplicate alert firing, duplicate probe result insertion, and wasted Lambda invocations.
Enabling reportBatchItemFailures (set via FunctionResponseTypes: ['ReportBatchItemFailures'] on the event source mapping, or via CDK's reportBatchItemFailures: true) lets the function return a structured response indicating which specific message IDs failed. Only those messages are retried.
// Lambda handler with partial batch failure support
import { SQSEvent, SQSBatchResponse, SQSBatchItemFailure } from 'aws-lambda';
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const failures: SQSBatchItemFailure[] = [];
for (const record of event.Records) {
try {
await processProbeResult(JSON.parse(record.body));
} catch (err) {
console.error('Failed to process message', record.messageId, err);
failures.push({ itemIdentifier: record.messageId });
// Do NOT rethrow — let other messages succeed
}
}
return { batchItemFailures: failures };
// Lambda retries only the failed messageIds; successful messages are deleted
};
// If all messages succeed, return empty batchItemFailures:
// { batchItemFailures: [] } ← all messages deleted
// If you return nothing (void), all messages are treated as succeeded
// If you throw, all messages are treated as failed (retried)
Key rules for partial failure handling:
- Never rethrow inside the per-message loop — catch errors and push the
messageIdto the failures array. - If the failures array contains all message IDs, the behavior is equivalent to throwing — all messages retry.
- Failed message IDs must match exactly the
messageIdfield from the SQS record (not the deduplication ID, not a custom ID). - For FIFO queues, a failed item in a message group blocks all subsequent messages in that group until the failed item is either processed or moves to the DLQ — partial failures in FIFO have higher blast radius than in standard queues.
Batch size for FIFO queues
FIFO queue Lambda ESM batchSize is capped at 10. A single Lambda invocation receives up to 10 messages from one or more message groups. Within a single invocation, messages within the same group are delivered in FIFO order; across groups, ordering is not guaranteed within the batch.
For AliveMCP's per-endpoint probe pipeline with FIFO queues, a batchSize=1 gives the simplest partial-failure semantics (no batch to worry about) at the cost of 10× more Lambda invocations. A batchSize=10 with reportBatchItemFailures=true is more efficient — process each record independently in a loop and report failures individually.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Single bad message causes all 9 healthy batch members to retry | reportBatchItemFailures not enabled — Lambda throws on any error, retrying entire batch | Enable reportBatchItemFailures: true and return batchItemFailures array with only the failed messageId |
| SendMessageBatch returns partial success with some Failed entries | Individual messages in the batch exceeded size, had malformed deduplication IDs, or hit a service-side throttle | Check SenderFault: true = fix the message; false = retry the specific failed entries with exponential backoff |
| Lambda invoked with only 1–2 messages despite batchSize=10 | Queue has fewer than 10 messages at the time Lambda polls — Lambda invokes as soon as any messages are available, even fewer than batchSize | Expected behaviour; set maximumBatchingWindowInSeconds to accumulate a larger batch if invocation count matters more than latency |
| FIFO queue: message group stalled — subsequent messages not delivered | A failed message in the group was not reported in batchItemFailures, so Lambda did not retry it; or it reached maxReceiveCount and went to the DLQ, but the DLQ is not monitored | Alarm on FIFO DLQ ApproximateNumberOfMessagesVisible > 0; add manual redrive or consumer to unblock the group |
| DeleteMessageBatch fails for some handles | Handles expired (visibility timeout elapsed; message reappeared and was received by another consumer) or message already deleted | Check resp.Failed after DeleteMessageBatch; treat ReceiptHandleIsInvalid as benign (message already processed) |