Photo by Franck V. on Unsplash
AWS SDK v3 retry configuration is set per client: maxAttempts (default 3, counting the first call) and retryMode (standard by default, or adaptive). Timeouts live on the request handler: connectionTimeout and requestTimeout both default to 0, meaning no timeout. Add throwOnRequestTimeout: true, and use an abortSignal for an overall deadline.
This guide is for Node.js and TypeScript developers on AWS SDK for JavaScript v3 who need calls that fail fast instead of hanging, retry the right errors, and fit inside a Lambda timeout. You’ll get every default, the retry rules, and working settings for servers, scripts and Lambda functions.
The numbers below come from the SDK source (@smithy/core 3.35 and @smithy/node-http-handler 4.12, checked in September 2026). The AWS SDK for JavaScript v3 client configuration docs on GitHub describe the same options.
What does AWS SDK v3 do by default?
| Setting | Default | Where it’s set |
|---|---|---|
| Total attempts | 3 (1 call + 2 retries) | maxAttempts, AWS_MAX_ATTEMPTS, max_attempts in ~/.aws/config |
| Retry mode | standard |
retryMode, AWS_RETRY_MODE, retry_mode |
| Backoff base | 100 ms; 500 ms for throttling errors | Custom retryStrategy |
| Maximum backoff | 20 seconds | Custom retryStrategy |
| Retry quota | 500 tokens per client | Built in |
| Connection timeout | 0 (none) | requestHandler.connectionTimeout |
| Request timeout | 0 (none), and only a warning unless throwOnRequestTimeout is set |
requestHandler.requestTimeout |
| Keep-alive and sockets | keepAlive: true, maxSockets: 50 |
requestHandler.httpsAgent |
Watch two rows. With no timeouts, a call to an endpoint that accepts the connection but never answers waits indefinitely. And requestTimeout on its own only logs a warning in current @smithy/node-http-handler releases; the request keeps going until you also set throwOnRequestTimeout: true.
Which errors does the SDK retry?
The standard strategy classifies every failure and retries only two classes:
- Throttling: HTTP 429, or error names such as
ThrottlingException,TooManyRequestsException,RequestLimitExceededandSlowDown. These back off from a 500 ms base. - Transient: HTTP 500, 502, 503 and 504;
TimeoutError,RequestTimeoutandRequestTimeoutException; Node.js network errorsECONNRESET,ECONNREFUSED,EPIPE,ETIMEDOUT,EHOSTUNREACH,ENETUNREACH,ENOTFOUNDandEAI_AGAIN; and errors the service model marks as retryable.
Everything else fails immediately: AccessDenied, ValidationException and other 4xx client errors, plus AbortError from your own abort signal. Requests with a streaming body, such as a PutObject fed from a file stream, aren’t retried either, because the stream can’t be replayed. The guide to upload large files and streams to S3 with SDK v3 covers @aws-sdk/lib-storage, which retries individual parts instead.
The delay before retry n (counting from 0) is a random value between 0 and min(base × 2n, 20 s), known as full jitter. With the defaults, the first retry after a 503 waits up to 100 ms and the second up to 200 ms. When the service sends a Retry-After header, the SDK waits at least that long, capped at 5 seconds beyond the computed delay.
The retry quota protects a struggling service. Each client starts with 500 tokens; a retry after a transient error costs 10, a retry after throttling costs 5, and successful calls pay tokens back. When the bucket is empty, the SDK stops retrying and returns the error at once. Recent releases also contain an opt-in flag, AWS_NEW_RETRIES_2026=true, that changes these constants; the numbers on this page are the defaults with it unset. The retry implementation in smithy-typescript is short and worth reading if you need the exact rules.
Prerequisites
- Node.js 20 or later. AWS SDK v3 releases from 3.968.0 require it.
- The
@aws-sdk/client-*packages you already use, plus@smithy/node-http-handlerand@smithy/util-retryif you import their classes directly. - Code on v3. If it still uses
aws-sdkv2, where the option wasmaxRetries, follow the steps to migrate a Node.js app from AWS SDK v2 to v3 first, or convert single files with the free AWS SDK v2 to v3 converter.
AWS SDK v3 retry configuration, step by step
- Pick the attempt countKeep 3 for interactive requests. Raise it for batch jobs that can wait, such as nightly scripts against throttled APIs.
- Choose a retry mode
standardfor most code;adaptivewhen one process hammers a single API and hits throttling. - Set connection and request timeoutsSo a dead endpoint fails in seconds and becomes a retryable
TimeoutError. - Add a deadline per callAn
abortSignalcaps the total time across all attempts and backoff. - Create the client onceShare it across requests so the connection pool and retry quota do their job.
Steps 1 and 2: maxAttempts and retryMode
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
// Up to 5 attempts in total (1 call + 4 retries), with client-side rate limiting.
export const ddb = new DynamoDBClient({
region: "us-east-1",
maxAttempts: 5,
retryMode: "adaptive",
});
Adaptive mode adds a client-side rate limiter: after a throttling error it lowers the client’s sending rate and makes later calls wait for a token, then raises the rate again on success. That helps a single-process job, such as a script that calls IAM hundreds of times. It’s per client instance, so it does little for a fleet of Lambda functions, and it adds latency after a throttle. Batch jobs against Amazon Bedrock are a typical case, because Bedrock enforces per-model request and token quotas; the guide to invoke Bedrock models with AWS SDK v3 uses adaptive mode for that reason.
To change settings without code, set AWS_MAX_ATTEMPTS=5 and AWS_RETRY_MODE=adaptive, or add max_attempts and retry_mode to the profile in ~/.aws/config. Values in the constructor win. The same profile also decides which credentials the client uses, as the guide to AWS SDK v3 credential providers such as fromIni and fromSSO explains.
Custom backoff with ConfiguredRetryStrategy
import { S3Client } from "@aws-sdk/client-s3";
import { ConfiguredRetryStrategy } from "@smithy/util-retry";
// 6 attempts in total; wait 1 s, 2 s, 3 s, 4 s, 5 s before each retry.
// A retryStrategy replaces maxAttempts and retryMode, which are then ignored.
export const s3 = new S3Client({
region: "us-east-1",
retryStrategy: new ConfiguredRetryStrategy(6, (attempt: number) => attempt * 1_000),
});
It still retries only throttling and transient errors, and still uses the retry quota.
How do you set connection and request timeouts?
import { S3Client } from "@aws-sdk/client-s3";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { Agent } from "node:https";
export const s3 = new S3Client({
region: "us-east-1",
maxAttempts: 4,
requestHandler: new NodeHttpHandler({
connectionTimeout: 3_000, // TCP + TLS connect must finish within 3 s
requestTimeout: 10_000, // response headers must arrive within 10 s
throwOnRequestTimeout: true, // turn the requestTimeout warning into a TimeoutError
httpsAgent: new Agent({ keepAlive: true, maxSockets: 100 }),
}),
});
What each timeout measures:
connectionTimeout: time to get a connected socket. It only applies to new connections; a reused keep-alive socket is already connected.requestTimeout: time from sending the request until response headers arrive. It doesn’t cover reading a large response body, so a slow S3 download isn’t cut off by it.
A timeout surfaces as a TimeoutError, which the retry strategy treats as transient, so a 10-second requestTimeout with 4 attempts can take a little over 40 seconds before the error reaches you. Plan the numbers together. If you only need timeouts, a plain object works and the SDK builds the handler for you:
import { SQSClient } from "@aws-sdk/client-sqs";
// Same handler options as a plain object; the SDK builds the NodeHttpHandler.
export const sqs = new SQSClient({
region: "us-east-1",
requestHandler: {
connectionTimeout: 3_000,
requestTimeout: 30_000, // longer than a 20 s long poll
throwOnRequestTimeout: true,
},
});
Long polling is the classic case for a longer requestTimeout: an SQS ReceiveMessage with WaitTimeSeconds: 20 legitimately takes 20 seconds. You can also override the timeout for a single call with client.send(command, { requestTimeout: 25_000 }). The guide to send and receive SQS messages with AWS SDK v3 in TypeScript builds a full consumer loop around these settings.
How do you cap the total time of a call?
Timeouts apply per attempt. To bound the whole operation, including retries and backoff, pass an AbortSignal:
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
const ddb = new DynamoDBClient({ region: "us-east-1", maxAttempts: 4 });
export async function getOrder(orderId: string) {
try {
const res = await ddb.send(
new GetItemCommand({ TableName: "orders", Key: { pk: { S: orderId } } }),
{ abortSignal: AbortSignal.timeout(2_500) }, // one deadline for all attempts
);
console.log(`attempts=${res.$metadata.attempts} retryDelayMs=${res.$metadata.totalRetryDelay}`);
return res.Item ?? null;
} catch (err) {
if (err instanceof Error && err.name === "AbortError") {
throw new Error(`getOrder(${orderId}) gave up after 2.5 s`, { cause: err });
}
throw err;
}
}
When the signal fires, the in-flight request is destroyed and the call rejects with an error named AbortError, with the signal’s reason as its cause. AbortError is never retried. Every result also carries $metadata.attempts and $metadata.totalRetryDelay, and errors carry the same fields, which is the cheapest way to see retries in your logs.
Retries and timeouts in AWS Lambda
A Lambda function’s timeout defaults to 3 seconds and can be raised to 900 seconds. The SDK knows nothing about it: a hung call runs until Lambda kills the invocation, with no useful error. Three rules fix that:
- Create clients outside the handler so warm invocations reuse connections instead of paying a new TLS handshake each time. Cache start-up lookups the same way, for example when you get a Secrets Manager secret value with AWS SDK v3.
- Keep per-attempt timeouts short, a small fraction of the function timeout, so a retry still fits.
- Derive a deadline from the remaining time with
context.getRemainingTimeInMillis(), leaving a margin to log and return.
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
// Minimal shape of the Lambda context object used here.
type LambdaContext = { getRemainingTimeInMillis(): number };
// Created once per execution environment and reused across invocations.
const ddb = new DynamoDBClient({
maxAttempts: 3,
requestHandler: { connectionTimeout: 1_000, requestTimeout: 3_000, throwOnRequestTimeout: true },
});
export const handler = async (event: { id: string }, context: LambdaContext) => {
// Stop AWS calls 500 ms before Lambda would kill the invocation,
// so the function can log and return a clean error instead of timing out.
const budget = Math.max(context.getRemainingTimeInMillis() - 500, 0);
await ddb.send(
new PutItemCommand({ TableName: "events", Item: { pk: { S: event.id } } }),
{ abortSignal: AbortSignal.timeout(budget) },
);
return { ok: true };
};
Also remember that the caller may retry too. Asynchronous invocations and event source mappings retry failed invocations on their own, so an SDK call that already retried 3 times can run again from the top; make handlers idempotent. Our pages on how to investigate Lambda errors with CloudWatch and getting Lambda invocation counts for the last 24 hours help you spot timeouts after the fact. For calling another function, see how to invoke a Lambda function with AWS SDK v3 in TypeScript; a synchronous invoke needs a requestTimeout longer than the target function’s timeout.
Example: watch every attempt and retry
To confirm your settings behave as intended, add a middleware in the finalizeRequest step with low priority. It runs inside the retry loop, so it sees each attempt:
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
// Print every attempt the SDK makes, including retries.
s3.middlewareStack.add(
(next) => async (args) => {
const started = Date.now();
try {
const result = await next(args);
console.log(`attempt ok in ${Date.now() - started} ms`);
return result;
} catch (err) {
console.log(`attempt failed in ${Date.now() - started} ms: ${(err as Error).name}`);
throw err;
}
},
{ step: "finalizeRequest", name: "logEachAttempt", priority: "low" },
);
async function main(): Promise<void> {
const res = await s3.send(new ListBucketsCommand({}));
console.log(`attempts=${res.$metadata.attempts}, buckets=${res.Buckets?.length ?? 0}`);
}
main().catch(console.error);
attempt failed in 3004 ms: TimeoutError
attempt ok in 188 ms
attempts=2, buckets=14
The first attempt hit a 3-second requestTimeout; the SDK treated the TimeoutError as transient and succeeded on the second attempt.
Permissions needed
Retry and timeout settings need no IAM permissions of their own. A retry repeats the same API action, so it’s authorized exactly like the first attempt. The reverse matters more: AccessDenied is a client error and is never retried, so no amount of maxAttempts fixes it. Follow the steps to troubleshoot AWS IAM access denied errors, and use the method to find the IAM actions your AWS SDK for JavaScript code needs when a new call is denied.
Common mistakes and how to fix them
| Symptom | Cause and fix |
|---|---|
| Calls hang for minutes | No timeouts set, or requestTimeout without throwOnRequestTimeout: true. Set both, plus connectionTimeout. |
| Warning: “a request has exceeded the configured … requestTimeout” | Same cause: the handler only logs. Add throwOnRequestTimeout: true. |
| Retries after moving from v2 are off by one | v2 maxRetries: 5 equals v3 maxAttempts: 6. |
maxAttempts seems ignored |
A custom retryStrategy is set; it overrides maxAttempts and retryMode. |
| Throttling errors under load | Many parallel calls against a per-account rate limit. Use adaptive mode, batch APIs, or request a higher quota; the guide to monitor AWS service quota usage shows how to check. |
Socket warnings about maxSockets |
More concurrent requests than the default 50 sockets. Raise maxSockets or limit concurrency. |
| A new client per request | No connection reuse and a fresh retry quota every time. Create clients once at module level. |
Limits: what retries and timeouts can’t do
- They don’t make non-idempotent calls safe. If a timeout fires after the service processed a request, the retry sends it again. Use idempotency tokens where the API offers them, such as
ClientTokenon many EC2 calls. - They don’t retry streaming uploads or client errors.
requestTimeoutdoesn’t cap the time spent reading a response body.- Adaptive mode limits one client in one process, not your account’s total request rate.
Porting Python code? botocore’s retries settings map to the same two options; the guide to port a Python boto3 script to Node.js with AWS SDK v3 covers the rest, and the boto3 to AWS SDK v3 converter translates the code. For more working v3 code, browse the AWS SDK v3 examples in TypeScript. ChatWithCloud itself generates AWS SDK v2 code when it answers questions, as how ChatWithCloud works explains, so none of these v3 settings apply to it.
Frequently asked questions
What is the default retry count in AWS SDK for JavaScript v3?
maxAttempts defaults to 3, which means the original call plus up to 2 retries, in standard retry mode.
What is the default timeout in AWS SDK v3?
There is none. connectionTimeout and requestTimeout both default to 0 in the Node.js request handler. Set them yourself, with throwOnRequestTimeout: true.
How do I disable retries in AWS SDK v3?
Set maxAttempts: 1 on the client. The call is then made once and any error is returned immediately.
Where does aws sdk v3 retry configuration go for all clients at once?
Use AWS_MAX_ATTEMPTS and AWS_RETRY_MODE, or max_attempts and retry_mode in the shared config profile. Timeouts must be set per client.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud
