What it means
ECONNRESET means a TCP connection that was already established was torn down by the other end with an RST packet rather than closed politely. Your side finds out by trying to use it: the read or write fails, and the error names the syscall rather than anything about why the peer went away.
What makes this a characteristically serverless problem is the execution environment lifecycle. Lambda freezes a container after an invocation and thaws it for the next, which is what makes warm starts fast. But freezing does not close sockets — it suspends the process holding them. A connection pool created at module scope, deliberately, to be reused across invocations, sits there with open sockets while nothing is running. If the peer's idle timeout is sixty seconds and the function is invoked every ninety, that pool is holding a dead connection every single time it wakes up, and the first query of each warm invocation discovers it.
That produces a failure pattern that reads as random and is not. Cold starts always work, because they open fresh connections. Busy periods work, because the sockets never idle long enough. The failures cluster on the first request after a lull, at a rate that tracks how sparse your traffic is — which is exactly the shape that survives every load test and appears in production during the quiet hours.
The fix follows directly from the mechanism, and it is worth being clear about which one you need. Retrying works because a reset from a stale socket succeeds immediately on a new one. Setting your pool's idle timeout below the peer's works better, because it removes the race rather than recovering from it. And for relational databases the underlying tension — every execution environment holding its own connections, none of them shareable — is what RDS Proxy exists to solve. Distinguishing a stale-socket reset from a peer that is genuinely failing comes down to whether the resets fall on first-calls-after-idle or arrive all at once across every concurrent execution, which is a question about the invocation's position in its container's life rather than about the error text.
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 when the error escapes the handler; a retried and recovered reset is not counted at all. LogStitch classified the example below from its log level rather than an extracted error type, so it reports the failure as caught — check your own stack to see whether anything handled it.
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
9 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
Error5e2b71c8-9034-4a17-8f6d-2c1a7b0e4d93
- 17:05:44.114PLAT
START RequestId: 5e2b71c8-9034-4a17-8f6d-2c1a7b0e4d93 Version: $LATEST
- 17:05:44.118INFO
INFO Warm container, reusing pg pool
- 17:05:44.121DEBUG
DEBUG Querying account balances tenant=acme
- 17:05:44.188ERROR
ERROR Error: read ECONNRESET at TCP.onStreamRead (node:internal/stream_base_commons:217:20) at Connection.emit (node:events:518:28) at /var/task/node_modules/pg/lib/client.js:132:734 lines - 17:05:44.402PLAT
END RequestId: 5e2b71c8-9034-4a17-8f6d-2c1a7b0e4d93
- 17:05:44.402PLAT
REPORT RequestId: 5e2b71c8-9034-4a17-8f6d-2c1a7b0e4d93 Duration: 288.44 ms Billed Duration: 289 ms Memory Size: 512 MB Max Memory Used: 131 MB Status: error
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
The important question is whether the reset happens on the first call of an invocation or a later one. A reset on the first outbound call of a warm container is the classic stale-keepalive case — the connection was cached from a previous invocation and the peer dropped it during the gap. A reset mid-invocation, after other calls have succeeded, points at the peer actively closing rather than at staleness.
Check the timing too. Resets that cluster after a period of low traffic, or that always fall on the first request after a quiet spell, are almost always idle-timeout related. Resets spread evenly across a busy period usually mean the peer is shedding load or restarting.
Causes, most likely first
A cached connection went stale while the execution environment was frozen
Check whether the failing call is the first outbound request of that invocation on a warm container. Lambda freezes the environment between invocations, so a keep-alive socket held at module scope can sit idle for minutes; the peer times it out and the function discovers this only by writing to a dead socket.
The peer's idle timeout is shorter than the gap between invocations
Compare the destination's idle-timeout setting against your traffic pattern. Load balancers commonly reset idle connections after around sixty seconds, and a function invoked less often than that will meet a closed socket on nearly every warm start.
A database restarted, failed over, or dropped the connection
Look for the reset coinciding across many invocations at once rather than appearing sporadically. A simultaneous burst across every concurrent execution is a server-side event — a failover, a restart, a max-connections limit reached — rather than anything about one socket.
A proxy or NAT gateway timed the flow out
Check whether the function is VPC-attached and its traffic passes through a NAT gateway. NAT gateways drop idle flows after a period, and subsequent packets on that flow get a reset — the same symptom as a peer timeout, from a different component.
TLS negotiation failed rather than the connection breaking
Read the exact message. "Client network socket disconnected before secure TLS connection was established" is a reset during the handshake, which points at protocol or cipher mismatch rather than at an idle socket.
Fixes
Retry idempotent requests — a stale-socket reset succeeds immediately on the second try
A reset caused by a dead cached connection is transient by construction: the retry opens a new socket and works. Retry safe operations rather than failing the invocation, and keep the attempts few and fast so a genuinely down peer still fails quickly.
jsasync function withRetry(operation, attempts = 3) {
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await operation();
} catch (error) {
const transient = ["ECONNRESET", "EPIPE", "ETIMEDOUT"].includes(error.code);
if (!transient || attempt === attempts) throw error;
await new Promise((r) => setTimeout(r, 2 ** attempt * 50));
}
}
}
Keep the connection pool's idle timeout below the peer's
If your client discards a socket before the peer does, you never write to a dead one. Setting the pool's idle timeout under the load balancer's or database's is the fix that removes the race rather than retrying around it.
jsimport pg from "pg";
// Module scope: reused across invocations on this container.
const pool = new pg.Pool({
max: 1, // one socket per execution environment
idleTimeoutMillis: 30_000, // below a typical 60s peer idle timeout
connectionTimeoutMillis: 3_000
});
Use a connection proxy for relational databases
Lambda's concurrency model and traditional database connection pooling are a poor fit — each execution environment holds its own connections and none of them can be shared. RDS Proxy holds the pool outside the function, which removes both the reset problem and the max-connections problem underneath it.
yamlResources:
DbProxy:
Type: AWS::RDS::DBProxy
Properties:
EngineFamily: POSTGRESQL
IdleClientTimeout: 1800
RequireTLS: true
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.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- AWS Lambda Developer Guide — Lambda execution environment lifecycle
- AWS Lambda Developer Guide — Using Amazon RDS Proxy with Lambda
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.