ReadTimeoutError: Read timed out. (read timeout=15)

Short answer

A connection was established and the response did not finish in time. This is not a connectivity failure — the network worked — which is why it needs a different fix from ETIMEDOUT, and why the number in brackets is the most useful part.

What it means

There are two clocks on any HTTP request, and confusing them sends you to fix the wrong thing. The connect timeout governs establishing the connection; when it expires you get ETIMEDOUT or a connect error, and the cause is routing, security groups, or DNS. The read timeout governs waiting for the response once the connection is up. ReadTimeoutError is the second one, and it means the network was fine — something answered, it was just not finished in time.

That distinction is worth holding onto because the fixes have nothing in common. A connect failure in a VPC is a NAT gateway or a security group. A read timeout is a budget that does not match the work, and no amount of network configuration will change it.

The most useful thing about this error on Lambda is that you are lucky to be seeing it at all. The AWS SDKs default to read timeouts that are generous relative to a typical function's own timeout, which means the Lambda usually dies first — and a Lambda that times out reports Task timed out after N seconds with no mention of the downstream call that was hanging. Setting client timeouts shorter than the function's is what converts a silent, unattributable timeout into a logged error naming the operation, which is a diagnostic technique as much as a reliability one.

Retries make the arithmetic less obvious than it looks. Botocore retries by default, so the budget that matters is attempts multiplied by the read timeout, plus whatever else the handler does. Three attempts at fifteen seconds is forty-five seconds of potential waiting from one client — comfortably past a thirty-second function timeout, which is how a well-intentioned retry configuration produces a Lambda timeout instead of a useful error.

The last thing worth checking is where in the container's life the failure lands. A client constructed inside the handler negotiates a fresh TLS connection every invocation; one at module scope reuses it across every request that execution environment serves. When a timeout is marginal, that handshake is the difference between failing on cold starts and never failing at all.

Depends

CloudWatch’s Errors metric: Whether CloudWatch’s Errors metric counts this depends on whether the error escaped your handler, which the log line alone does not settle. Counted only when the error escapes the handler; the AWS SDKs retry read timeouts, so many are absorbed. LogStitch classified the example below from its log level rather than an extracted error type.

What it looks like in CloudWatch

This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.

CloudWatch Logs Before

7 raw lines, in the order CloudWatch delivered them.

2026-09-01T13:40:12.108Z START RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53 Version: $LATEST
2026-09-01T13:40:12.114Z 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53 INFO Fetching export manifest
2026-09-01T13:40:27.402Z 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53 ERROR ReadTimeoutError: Read timed out. (read timeout=15)
File "/var/task/lambda_function.py", line 34, in lambda_handler
File "/var/runtime/botocore/httpsession.py", line 464, in send
2026-09-01T13:40:27.588Z END RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53
2026-09-01T13:40:27.588Z REPORT RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53 Duration: 15480.11 ms Billed Duration: 15481 ms Memory Size: 512 MB Max Memory Used: 118 MB Status: error

LogStitch After

The same lines, grouped into the invocation they belong to.

Error9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53dur 15.48sbilled 15.48smem 118/512MBlogs 5
  1. 13:40:12.108PLAT
    START RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53 Version: $LATEST
  2. 13:40:12.114INFO
    INFO	Fetching export manifest
  3. 13:40:27.402ERROR
    ERROR	ReadTimeoutError: Read timed out. (read timeout=15)
        File "/var/task/lambda_function.py", line 34, in lambda_handler
        File "/var/runtime/botocore/httpsession.py", line 464, in send
    3 lines
  4. 13:40:27.588PLAT
    END RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53
  5. 13:40:27.588PLAT
    REPORT RequestId: 9d3f1a68-5c02-4e77-b419-6a8e2c7b0f53	Duration: 15480.11 ms	Billed Duration: 15481 ms	Memory Size: 512 MB	Max Memory Used: 118 MB	Status: error
Error
Status
15.48s
Duration
15.48s
Billed
118/512MB
Memory
77%
Headroom
No
Cold start
LogLevelError
Error type

The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.

How to confirm it from the logs

Note the timeout value in the message and compare it to the elapsed time before the error. A read timeout=15 failing after roughly fifteen seconds means the client's own budget ran out, not that anything was unreachable.

Then compare it against the Lambda's own timeout. If the SDK's read timeout is longer than the function's, the function dies first and you never see this at all — you get Task timed out instead, with no indication that a downstream call was the reason. Seeing this error is in that sense good news: it means the client gave up in time to tell you something.

Look for the retry pattern too. Botocore's default is to retry, so a single logged ReadTimeoutError usually means several attempts were made and all of them ran out — and the duration on the REPORT line includes all of them.

Causes, most likely first

1

The downstream operation genuinely takes longer than the timeout

How to confirm

Identify the call from the stack trace and consider its normal duration. A large S3 object, a DynamoDB scan, a Textract or Bedrock call — these routinely exceed the fifteen-second default that suits a small API request.

2

The default read timeout is being used unchanged

How to confirm

Check whether the client is constructed with an explicit Config. Botocore's default is 60 seconds and many wrappers reduce it; either way, a default chosen for a generic case is unlikely to suit both a GetItem and a multi-megabyte download.

3

Retries are consuming the function's budget

How to confirm

Multiply the read timeout by the retry count and compare it to the function's timeout. Four attempts at fifteen seconds is a minute, so a function with a thirty-second timeout dies mid-retry and reports a Lambda timeout rather than this.

4

A cold TLS handshake is being counted

How to confirm

Check whether the failure lands on the first call of a cold start. Establishing a connection and negotiating TLS adds latency that a warm client does not pay, so a timeout tight enough to be marginal will fail cold and succeed warm.

Fixes

Fix 1

Set timeouts per client, sized to what that client does

One timeout cannot suit every call. Give the client doing large transfers a generous budget and the one doing small lookups a tight one, so a slow download does not need the same patience as a key-value read.

pythonimport boto3
from botocore.config import Config

# Small, fast operations: fail quickly so a retry has time to succeed.
ddb = boto3.client("dynamodb", config=Config(
    connect_timeout=2, read_timeout=5,
    retries={"max_attempts": 3, "mode": "adaptive"},
))

# Large transfers: a longer budget, fewer attempts.
s3 = boto3.client("s3", config=Config(
    connect_timeout=2, read_timeout=60,
    retries={"max_attempts": 2, "mode": "standard"},
))
Fix 2

Keep the total retry budget inside the function's timeout

Attempts multiply the timeout. The whole budget — attempts times read timeout, plus your own work — has to fit inside the Lambda's timeout, or the function is killed mid-retry and you lose the error that would have explained it.

python# 3 attempts x 5s read timeout = 15s worst case for this client,
# comfortably inside a 30s function timeout with room for the handler.
config = Config(read_timeout=5, retries={"max_attempts": 3})
Fix 3

Build clients at module scope so the handshake is paid once

A client constructed inside the handler re-establishes its connection on every invocation. At module scope the connection is reused across invocations on that execution environment, which removes the cold-handshake latency from every request after the first.

pythonimport boto3

# Module scope: created once per execution environment.
s3 = boto3.client("s3")

def lambda_handler(event, context):
    return s3.get_object(Bucket=event["bucket"], Key=event["key"])
Fix 4

Stop transferring so much in one call

Where the operation is genuinely large, a longer timeout only defers the problem. Paginating a query, requesting a byte range rather than a whole object, or moving the work to a step function keeps any single call bounded.

Also seen as

The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.

botocore.exceptions.ReadTimeoutErrorConnectTimeoutError: Connect timeout on endpoint URLTimeoutError: Socket timeoutlambda boto3 read timeoutlambda botocore readtimeouterror

Errors that show up alongside this one, or that people mistake for it.

References

LogStitch finds this automatically, across every invocation in your account.

Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.