MongoDB connection refused (tested)
MongoDB connection errors in Node almost always surface as a MongoServerSelectionError, not a plain ECONNREFUSED. The driver hides the instant OS-level refusal behind its own retry loop and a 30-second default timeout. We ran a real MongoDB in Docker, then measured the closed-port error, the real wall-clock wait, mongoose's wrapper, and what happens to in-flight queries when a live server disappears mid-session.
1. The error you are probably seeing
With the official mongodb driver (measured, v7.7.0), connecting to a port nothing is listening on throws a MongoServerSelectionError, not a raw ECONNREFUSED. We connected to port 27019 (nothing listening) and let the driver run to its default timeout:
MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27019
at Topology.selectServer (.../mongodb/lib/sdam/topology.js:348:38)
at async Topology._connect (.../mongodb/lib/sdam/topology.js:220:28)
at async MongoClient.connect (.../mongodb/lib/mongo_client.js:195:13)
{
errorLabelSet: Set(3) { 'SystemOverloadedError', 'RetryableError', 'ResetPool' },
[cause]: Error: connect ECONNREFUSED 127.0.0.1:27019 {
errno: -111, code: 'ECONNREFUSED', syscall: 'connect'
}
}
The real ECONNREFUSED is still there — it is the .cause of the error, and it fires in a few milliseconds, same as Postgres and Redis. But the driver does not throw it to your code right away. It treats the first refusal as "this one server is bad" and keeps retrying server selection against the whole topology until serverSelectionTimeoutMS runs out, which defaults to 30000. Only then does it give up and throw MongoServerSelectionError, with the original ECONNREFUSED attached as .cause.
2. Our test setup
This box has Docker but no mongod or mongosh binaries (which mongod mongosh returned nothing). We ran a real, throwaway MongoDB and drove it with Node instead of the Mongo shell.
$ docker run --rm -d --name forever-mongotest -p 27018:27017 mongo:7
$ docker logs forever-mongotest | tail -1
{"msg":"Waiting for connections","attr":{"port":27017,"ssl":"off"}}
mongod was already "Waiting for connections" by the time the pull finished and the container started (image was cached after the first pull). We connected the mongodb driver to the live container, to a closed port (27019), then stopped the live container mid-session with docker stop and started a fresh one on the same port 5 seconds later, with no reconnect code in the app.
3. Results: a 30-second default wait, confirmed by the clock
| Scenario | What we did | Exact result | Wall-clock time |
|---|---|---|---|
| Happy path | Connected to live mongo:7 on 27018 | Connected, buildInfo returned version 7.0.43 | 26 ms |
| Closed port, default options | Connected to port 27019, no options | MongoServerSelectionError: connect ECONNREFUSED 127.0.0.1:27019 | 30007 ms |
Closed port, serverSelectionTimeoutMS: 2000 | Same, with an explicit 2 s cap | Same error message, same error class | 2011 ms |
| Closed port, mongoose default | mongoose.connect() with serverSelectionTimeoutMS: 2000 | MongooseServerSelectionError: connect ECONNREFUSED 127.0.0.1:27019 | 2016 ms |
The first row proves this is not a hang on our end: a reachable MongoDB answers in milliseconds, same as Postgres or Redis. The second row is the one that catches people out — with no options set, our code sat for exactly 30 seconds before seeing any error at all, even though the OS refused the TCP connection in under 5 ms. We confirmed the internal retries are frequent, not one 30 s sleep: listening for serverHeartbeatFailed events during the 2 s run showed attempts at roughly 12 ms, 516 ms, 1018 ms and 1522 ms — about every 500 ms, which matches the driver's minHeartbeatFrequencyMS default used while establishing a connection.
4. mongoose wraps the same error, doesn't fix the wait
Most apps use mongoose, not the raw driver. We measured that mongoose's error has a different name, MongooseServerSelectionError instead of MongoServerSelectionError, but the same message and the same underlying 30 s default (we tested with an explicit 2 s cap for speed; the message text was identical: connect ECONNREFUSED 127.0.0.1:27019). If your code checks err.name or does err instanceof MongoServerSelectionError, that check will miss on mongoose — use err.name === 'MongooseServerSelectionError' or check for the driver error as err.cause/nested reason instead, or just match on the ECONNREFUSED substring in the message.
5. Live test: stop the server, watch queries queue, restart it
We connected to the live container, pinged it once a second, then ran docker stop forever-mongotest about 6.8 s in and started a fresh container on the same port roughly 5 s after that, with serverSelectionTimeoutMS: 20000 and no reconnect code.
[+6534ms] heartbeat ok
[+6810ms] heartbeat FAILED interrupted at shutdown
[+7315ms] heartbeat FAILED connect ECONNREFUSED 127.0.0.1:27018
[+7819ms .. +12344ms] heartbeat FAILED connect ECONNREFUSED 127.0.0.1:27018 (every ~500ms)
[+12846ms] heartbeat FAILED connection <monitor> to 127.0.0.1:27018 closed
[+13852ms] heartbeat ok
[+13859ms] ping -> OK (6 pings issued during the outage all resolved here, together)
Two things stand out. First, the driver recovered with zero reconnect code, the same as what we measured for ioredis against Redis — it kept retrying heartbeats roughly every 500 ms and picked the new server up the moment it started listening. Second, and different from both Postgres and Redis: the ping commands we issued during the outage did not reject or error individually. They stayed pending — queued behind server selection — and all resolved successfully together the instant the server came back, because the whole outage (about 7 s) was inside the 20 s serverSelectionTimeoutMS we'd set. An app that only logs on error would see nothing at all for an outage shorter than its timeout, just slower responses.
6. Common real causes
- mongod not started yet, crashed, or wrong port. Measured (section 3): gives
MongoServerSelectionErrorwrappingECONNREFUSED, but only after the fullserverSelectionTimeoutMSwait unless you've lowered it. - Docker Compose startup race. Per the pattern we measured for Postgres and Redis containers, not separately re-measured for Mongo here: mongod can take a few seconds to log "Waiting for connections," and an app that connects at container start hits the same ECONNREFUSED-under-the-hood case, just hidden behind a much longer default timeout than Postgres or Redis apps see.
- MongoDB Atlas (cloud) IP allowlist rejecting the connection. Per MongoDB's own documentation, not measured on this box (Atlas needs a cloud account): an IP not on the project's allowlist typically does not get an instant refusal at all. It times out during server selection, producing the same
MongoServerSelectionErrorclass but with a timeout-flavored underlying cause rather thanECONNREFUSED— and it can take the full 30 s default to surface, which is easy to mistake for a slow network rather than a blocked IP. - Wrong connection string host/replica set name. Per MongoDB docs, not measured: an
mongodb+srv://URI with a bad DNS record fails at the SRV lookup step, before server selection even starts, with a DNS-flavored error rather thanECONNREFUSED. - Auth failure once reachable. Per MongoDB docs, not measured here: a reachable server rejecting bad credentials returns a fast, explicit authentication error, not a
MongoServerSelectionError— if you're getting the slow 30 s error, the problem is reachability, not your username or password.
7. How MongoDB compares to Postgres and Redis
We measured ECONNREFUSED behaviour the same way for all three database layers on this box, and the timeout default is the real difference:
- Postgres (
pgdriver): throws a plainErrorwithcode: 'ECONNREFUSED'within a few milliseconds of a refused connection. No built-in retry. - Redis (
ioredis): emits an'error'event withECONNREFUSEDwithin milliseconds, then retries forever on its own with growing backoff, by default. - MongoDB (
mongodb/mongoose): the underlyingECONNREFUSEDalso happens within milliseconds, but it is buried inside internal retries for the fullserverSelectionTimeoutMS(30 s default) before your code ever sees an error. That is the single biggest practical difference: a MongoDB outage can look like your whole app hung for 30 seconds, when the network-level failure was actually instant.
8. How to fix it
- Lower
serverSelectionTimeoutMSfor anything latency-sensitive (we measured 2000 ms working exactly as expected, same error, 15x faster). Don't set it to near-zero in production: a brief Compose startup race or a GC pause can then look like a real outage. - Confirm the process is actually up:
docker psfor a mongod container, ordocker logs <container>for"Waiting for connections"(what we waited for in our test). - Check
err.cause(raw driver) or the error message text forECONNREFUSEDto tell "nothing is listening" apart from an Atlas allowlist timeout or a DNS/SRV failure — they land in the same error class but need different fixes. - On mongoose, match on
err.name === 'MongooseServerSelectionError', notMongoServerSelectionError— the names differ even though the message is the same. - Don't assume a silent app is a healthy one: we measured that queries issued mid-outage just queue and succeed quietly if the server returns before the timeout. Add your own query-level timeout or latency metric if you need to notice a degraded database before the 30 s mark.
- In Docker, make sure the published port and host match your connection string, and that the app and MongoDB containers share a network if either uses a service name instead of an IP.
For the plain cross-language version of this error outside a database context, see ERR_CONNECTION_REFUSED / ECONNREFUSED. If your connection hangs with no refusal at all rather than an eventual error, see ETIMEDOUT.
Frequently asked questions
What is the exact error for a MongoDB connection refused in Node?
With the official mongodb driver (measured, v7.7.0) it's a MongoServerSelectionError whose message is "connect ECONNREFUSED 127.0.0.1:<port>" and whose .cause holds the raw ECONNREFUSED. With mongoose (measured, v9.10.3) the class name is MongooseServerSelectionError instead, same message.
Why does my app take 30 seconds to fail when MongoDB is down?
That's the driver's default serverSelectionTimeoutMS. We measured that the real TCP refusal happens in milliseconds, but the driver retries server selection internally (about every 500ms, per our heartbeat measurements) and only throws after the full 30000ms default elapses. Set serverSelectionTimeoutMS lower if you need a faster failure; we measured 2000ms working with the same error.
Is this different from how Postgres or Redis fail?
Yes, measured on this box: pg and ioredis both surface ECONNREFUSED within milliseconds, with no mandatory wait. MongoDB's driver buries the same millisecond-fast refusal inside a 30-second default retry window before your code sees anything.
Does MongoDB automatically reconnect if the server comes back?
Yes, measured: we stopped a live container mid-session and started a fresh one 5 seconds later with no reconnect code. The driver kept retrying heartbeats on its own and picked the new server up as soon as it was listening. Queries issued during the outage stayed pending and resolved successfully once the server returned, rather than failing individually.
I'm on MongoDB Atlas and get a timeout, not ECONNREFUSED. Why?
Per MongoDB's documentation (not measured on this box, since Atlas needs a cloud account), an IP not on the project's allowlist is typically dropped rather than actively refused, so you get a timeout-style failure inside MongoServerSelectionError rather than an immediate ECONNREFUSED. It can still take up to the full serverSelectionTimeoutMS to surface. Check the Atlas Network Access allowlist before assuming it's a generic network problem.
Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0, the official `mongodb` npm driver v7.7.0, and `mongoose` v9.10.3, against a real mongo:7 Docker image (mongod 7.0.43) on 127.0.0.1:27018, and against a definitely-closed local port 27019. This box has no mongod or mongosh binaries, only Docker. MongoDB Atlas's IP-allowlist behaviour is labelled as such and was not measured. PlainHeaders is our extension; curl, Node.js, Docker and the mongodb/mongoose packages are free.