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:

PropertyShort polling (WaitTimeSeconds=0)Long polling (WaitTimeSeconds=1–20)
Servers sampledSubsetAll
Empty response on non-empty queuePossibleNot possible (all servers queried)
Blocking duration on empty queueImmediate returnUp to WaitTimeSeconds
Cost on idle queueHigh (many empty calls)Low (at most one call per WaitTimeSeconds)
Latency to receive available messageUp to one polling intervalSub-second (message returned as soon as it arrives)

Configuring long polling: queue vs per-call

There are two places to configure WaitTimeSeconds:

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:

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:

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

SymptomCauseFix
High SQS cost despite low message volumePolling loop using WaitTimeSeconds=0 — thousands of empty calls per hourSet WaitTimeSeconds=20 on ReceiveMessage calls or set ReceiveMessageWaitTimeSeconds=20 on the queue
socket hang up / ECONNRESET on ReceiveMessageHTTP client socket timeout shorter than WaitTimeSeconds — connection closed before long-poll returnsSet socket timeout to 25+ seconds in the SQS client HTTP handler
Messages received only every 20 seconds even when queue is always fullLong polling with WaitTimeSeconds=20 on a queue that is always non-empty — each call waits the full durationThis is normal behaviour; increase MaxNumberOfMessages (up to 10) or add concurrent pollers to drain faster
Lambda function invoked very infrequently despite messages in queueLambda ESM polling concurrency at minimum (1 poller) — only scales up when backlog is detectedThis is expected cold-start behaviour; Lambda ESM increases polling concurrency within ~1 minute of backlog growth
Empty responses on non-empty queueUsing short polling (WaitTimeSeconds=0) — only a subset of SQS servers sampledSwitch to long polling; empty responses disappear because all servers are queried