What it means
Of the four ways a network call can fail on Lambda, this is the one that tells you the most. ECONNREFUSED is a TCP reset: the SYN packet arrived at a live machine, the kernel there found nothing bound to that port, and it sent back a refusal. Every layer below the service — DNS, routing, NAT, security groups, network ACLs — did its job.
That eliminates most of the VPC problem space in one reading. If security groups were blocking the traffic you would get ETIMEDOUT, because a dropped packet produces silence rather than a refusal. If the name were wrong you would get ENOTFOUND. Getting a reset means the address is right, the path is open, and the only thing missing is a listener.
The single most common instance on Lambda is 127.0.0.1. A Lambda execution environment contains your function and the runtime — no database, no cache, no sidecar. Code that ran under Docker Compose, or on a server where Postgres happened to be local, carries a connection string that was perfectly correct in its original home and refers to nothing here. The error is immediate and the address in the message names the mistake outright.
The other case worth recognising is failover. During an RDS or ElastiCache failover the old host stays up while the service stops accepting connections, and DNS takes a moment to move. In that window every function gets a refusal, all at once, and then the problem disappears on its own. A burst of ECONNREFUSED across every concurrent execution followed by clean recovery is that pattern — and it is a case for a short bounded retry rather than for changing any configuration.
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. LogStitch classified the example below from its log level rather than an extracted error type, so it reports the failure as caught.
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.
LogStitch After
The same lines, grouped into the invocation they belong to.
Error91c4e7b2-5a38-4f01-bd26-7e0c9a3f1b58
- 09:02:44.114PLAT
START RequestId: 91c4e7b2-5a38-4f01-bd26-7e0c9a3f1b58 Version: $LATEST
- 09:02:44.118INFO
INFO Opening ledger connection
- 09:02:44.126ERROR
ERROR Error: connect ECONNREFUSED 10.0.4.22:5432 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16) at Client._connect (/var/task/node_modules/pg/lib/client.js:88:11)3 lines - 09:02:44.402PLAT
END RequestId: 91c4e7b2-5a38-4f01-bd26-7e0c9a3f1b58
- 09:02:44.402PLAT
REPORT RequestId: 91c4e7b2-5a38-4f01-bd26-7e0c9a3f1b58 Duration: 288.09 ms Billed Duration: 289 ms Memory Size: 512 MB Max Memory Used: 104 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 address and port in the message are the whole diagnosis. 127.0.0.1 or localhost means the code is trying to reach something in its own container — there is nothing else there, and this is almost always code carried over from a Docker or local development setup. A private address means the host is reachable and the service is not listening, or is listening on a different port.
Contrast it with the alternatives, because the distinction is precise and useful. ECONNREFUSED is immediate — the reset comes back in milliseconds. ETIMEDOUT takes seconds and means nothing answered at all, which points at routing or security groups. ENOTFOUND means the name never resolved. Getting a refusal means you are further along than either of the others.
Causes, most likely first
The code is connecting to localhost
Look for 127.0.0.1 or localhost in the address. A Lambda execution environment runs your handler and nothing else — no sidecar database, no local Redis, no Docker Compose peer. Code moved from a containerised environment frequently keeps a connection string that was correct there and cannot work here.
The service is listening on a different port
Compare the port in the message against the service's actual listener. A refusal means the host is up, so an off-by-one in a port number or a default assumed for the wrong engine — 5432 for Postgres against a MySQL instance on 3306 — produces exactly this.
The database or service is down, restarting, or failing over
Check whether the refusals cluster in time across many invocations rather than affecting one request. A simultaneous burst across every concurrent execution is a server-side event; the host is up enough to send resets but the service on it is not accepting connections.
The connection is going to the wrong host entirely
Check the address against the resource you intend. A stale private IP, a reader endpoint where a writer was meant, or a hostname resolving to a decommissioned instance all reach something that refuses rather than something that answers.
Fixes
Point the function at a real endpoint rather than a local one
There is no localhost service in a Lambda. Move the dependency to a managed endpoint and configure it through an environment variable, so the same code works in both places.
js// Carried over from docker-compose — nothing listens here in Lambda.
// const redis = createClient({ url: "redis://localhost:6379" });
const redis = createClient({ url: process.env.REDIS_URL });
Confirm what the host is actually listening on
A refusal proves the host is reachable, so the question is narrowly about the port and the service. Reading the endpoint back from the resource itself removes the guesswork.
bashaws rds describe-db-instances --db-instance-identifier orders-prod \
--query 'DBInstances[0].Endpoint'
Retry across a failover rather than failing the invocation
During an RDS failover the old host refuses connections for a short window before DNS moves. A short bounded retry rides through it; an unbounded one just burns your timeout.
jsasync function connectWithRetry(attempts = 3) {
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await pool.connect();
} catch (error) {
if (error.code !== "ECONNREFUSED" || attempt === attempts) throw error;
await new Promise((r) => setTimeout(r, 2 ** attempt * 100));
}
}
}
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 — Troubleshoot networking issues
- 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.