Guide · AWS SQS
SQS Long Polling for MCP Servers — WaitTimeSeconds, Cost Reduction, Lambda ESM
SQS offers two polling modes: short polling returns immediately whether or not messages are available, while long polling waits up to 20 seconds for a message to arrive before returning an empty response. For MCP monitoring pipelines that poll continuously, short polling generates thousands of empty API calls per hour — each one billed as a standard SQS request at $0.40 per million. Long polling eliminates most of these empty responses: if a message arrives during the wait window, it is returned in the same call. The tradeoff is latency — a long-polling call blocks for up to WaitTimeSeconds before returning empty, which is fine for background workers but unacceptable for interactive or real-time consumers. Lambda's Event Source Mapping uses long polling by default (20 seconds), which is why Lambda-triggered SQS processing has near-zero empty-response cost. For custom ECS or Node.js polling loops, configuring WaitTimeSeconds correctly is the single highest-ROI SQS cost optimisation available.
TL;DR
Set WaitTimeSeconds: 20 on ReceiveMessage calls (or set ReceiveMessageWaitTimeSeconds: 20 on the queue itself as the default). Lambda ESM already does this — no action needed for Lambda consumers. Short polling (WaitTimeSeconds: 0) is only appropriate when you need a non-blocking check (e.g., a health-check handler that must return in milliseconds, or a tight polling loop where the queue is always expected to have messages). Long polling does not change pricing per message — it only reduces the number of empty ReceiveMessage calls billed.
Short polling vs long polling: mechanics
SQS distributes messages across multiple servers. A short-poll ReceiveMessage request samples only a subset of those servers and returns any messages found immediately. If the sampled servers hold no messages, an empty response is returned — even if messages exist on other servers. This means short polling can return empty even on a non-empty queue, requiring more calls to drain the queue.
A long-poll ReceiveMessage request queries all SQS servers. If no messages are available, the connection stays open until:
- A message arrives (returned immediately),
WaitTimeSecondselapses (empty response returned), or- The HTTP connection times out at an intermediate proxy or load balancer (you should set HTTP client timeouts to at least 25 seconds to accommodate 20-second long-poll waits).
| Property | Short polling (WaitTimeSeconds=0) | Long polling (WaitTimeSeconds=1–20) |
|---|---|---|
| Servers sampled | Subset | All |
| Empty response on non-empty queue | Possible | Not possible (all servers queried) |
| Blocking duration on empty queue | Immediate return | Up to WaitTimeSeconds |
| Cost on idle queue | High (many empty calls) | Low (at most one call per WaitTimeSeconds) |
| Latency to receive available message | Up to one polling interval | Sub-second (message returned as soon as it arrives) |
Configuring long polling: queue vs per-call
There are two places to configure WaitTimeSeconds:
- Queue attribute
ReceiveMessageWaitTimeSeconds: sets the default for allReceiveMessagecalls on the queue that do not supply an explicitWaitTimeSeconds. Range: 0–20 seconds. - Per-call
WaitTimeSecondsparameter: overrides the queue default for that specific call. Range: 0–20 seconds.
If both are set, the per-call value takes precedence.
import { Queue, Duration } from 'aws-cdk-lib/aws-sqs';
import { SQSClient, ReceiveMessageCommand } from '@aws-sdk/client-sqs';
// Set long polling as the queue default
const probeQueue = new Queue(this, 'ProbeQueue', {
receiveMessageWaitTime: Duration.seconds(20), // CDK maps to ReceiveMessageWaitTimeSeconds
visibilityTimeout: Duration.seconds(90),
deadLetterQueue: { queue: dlq, maxReceiveCount: 3 },
});
// Manual polling loop (ECS consumer) — respects the queue default
const sqs = new SQSClient({});
async function pollLoop(queueUrl: string): Promise {
while (true) {
const resp = await sqs.send(new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10,
// WaitTimeSeconds omitted — uses queue's ReceiveMessageWaitTimeSeconds default (20s)
AttributeNames: ['All'],
MessageAttributeNames: ['All'],
}));
if (!resp.Messages || resp.Messages.length === 0) continue; // empty long-poll response
await Promise.all(resp.Messages.map(processMessage));
}
}
Lambda ESM and long polling
Lambda's Event Source Mapping polls your SQS queue using long polling with WaitTimeSeconds=20 by default. This is built in and cannot be changed through CDK or CloudFormation — it is a Lambda ESM implementation detail, not a configurable property.
Consequences:
- Lambda-triggered SQS queues generate at most ~3 empty ReceiveMessage calls per minute per polling process (one per 20 seconds). At production scale with thousands of Lambda concurrent executions, this is still negligible.
- Lambda ESM runs multiple polling processes concurrently. The number scales automatically with the number of messages in the queue, up to the
maximumConcurrencylimit or Lambda's service quota. - If your queue uses SSE with a customer-managed KMS key, Lambda's polling IAM role needs
kms:Decryptpermission on the key — long polling does not change this requirement.
For most AliveMCP probe pipelines using Lambda consumers, no long-polling configuration is required — the default ESM behavior already optimises empty-response cost.
When short polling is appropriate
Short polling (WaitTimeSeconds=0) is appropriate in two specific situations:
- Non-blocking queue depth check: a diagnostic handler that reports the approximate number of messages in the queue without waiting. Use
GetQueueAttributeswithApproximateNumberOfMessagesinstead — this is more accurate and does not consume a ReceiveMessage call. - High-throughput saturation polling: when the queue is always expected to have messages and you want maximum throughput from a multi-threaded consumer. In this mode, empty responses are rare (queue is never idle), so the cost of short polling is low. Long polling's 20-second wait would add latency during the brief idle gaps between bursts.
For MCP probe pipelines, the queue is typically idle between 60-second probe cycles. Long polling is almost always the right choice for probe-result queues.
HTTP client timeout configuration for long polling
Long-polling requests keep the HTTP connection open for up to 20 seconds. AWS SDK v3 sets the HTTP socket timeout to 0 (infinite) by default for Node.js when using the SQS client — but if you are running behind a load balancer or proxy with a short idle connection timeout, long-poll requests may be aborted prematurely, producing a socket hang up or ECONNRESET error.
import { SQSClient } from '@aws-sdk/client-sqs';
import { NodeHttpHandler } from '@smithy/node-http-handler';
const sqs = new SQSClient({
requestHandler: new NodeHttpHandler({
socketTimeout: 25_000, // 25 seconds — 5s margin above max 20s long-poll wait
connectionTimeout: 5_000, // 5 seconds for initial TCP connect
}),
});
On ECS tasks behind an ALB: set the ALB idle timeout to at least 25 seconds (ALB default is 60 seconds — this is fine by default, but worth verifying if your DevOps team has tuned it down).
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| High SQS cost despite low message volume | Polling loop using WaitTimeSeconds=0 — thousands of empty calls per hour | Set WaitTimeSeconds=20 on ReceiveMessage calls or set ReceiveMessageWaitTimeSeconds=20 on the queue |
socket hang up / ECONNRESET on ReceiveMessage | HTTP client socket timeout shorter than WaitTimeSeconds — connection closed before long-poll returns | Set socket timeout to 25+ seconds in the SQS client HTTP handler |
| Messages received only every 20 seconds even when queue is always full | Long polling with WaitTimeSeconds=20 on a queue that is always non-empty — each call waits the full duration | This is normal behaviour; increase MaxNumberOfMessages (up to 10) or add concurrent pollers to drain faster |
| Lambda function invoked very infrequently despite messages in queue | Lambda ESM polling concurrency at minimum (1 poller) — only scales up when backlog is detected | This is expected cold-start behaviour; Lambda ESM increases polling concurrency within ~1 minute of backlog growth |
| Empty responses on non-empty queue | Using short polling (WaitTimeSeconds=0) — only a subset of SQS servers sampled | Switch to long polling; empty responses disappear because all servers are queried |