A MongoDB connection string is one line with five parts, and almost every problem with one is in the fourth or fifth:
mongodb://appuser:s3cr3t@db.example.net:27017/shop?authSource=admin&retryWrites=trueScheme, credentials, host and port, default database, options. If the connection is refused, the host or the port is wrong or the server is not listening on an address you can reach. If authentication fails with credentials you know are right, the answer is almost always authSource, or a password containing a character that needed percent-encoding. Those two account for most of the support questions about MongoDB connectivity, and both are fixed in the URI rather than in the server.
This post takes the URI apart, covers mongodb+srv and when it does not apply, the options worth setting, how to connect from mongosh, Compass and the Node and Python drivers, and what each of the common errors is actually telling you.
The anatomy of a MongoDB URI#
| Part | Example | Notes |
|---|---|---|
| Scheme | mongodb:// | Or mongodb+srv:// for DNS seed lists |
| Credentials | appuser:s3cr3t@ | Optional; percent-encode both |
| Hosts | db.example.net:27017 | Comma-separated for a replica set |
| Default database | /shop | The database the driver uses if you do not name one |
| Options | ?authSource=admin | & separated, case-insensitive keys |
Two of those need a closer look before anything else.
The default database is the one a driver returns from client.db() with no argument, and, more importantly, it is the default value for authSource. Leaving it off is legal: mongodb://user:pass@host:27017/?authSource=admin is a complete URI, and the trailing slash before ? is required if you want options without naming a database.
The host list is where replica sets are declared. mongodb://a:27017,b:27017,c:27017/?replicaSet=rs0 tells the driver about three members; the driver discovers the rest of the topology itself, works out which one is currently primary, and re-routes writes when that changes. On a single standalone server there is one host and no replicaSet option, which is the common case for a database server you rent by itself. On RE:NODE the standalone lines are PostgreSQL and MongoDB, reached on the host and port shown on the plan, with a superuser password generated for that server rather than a published default, so your first URI is that host, that port, and those credentials.
Passwords, percent-encoding and the error nobody expects#
A URI is a URI, so any character in the username or password that has a meaning inside one has to be percent-encoded. Generated passwords are full of them.
| Character | Encoded |
|---|---|
: | %3A |
/ | %2F |
? | %3F |
# | %23 |
[ and ] | %5B and %5D |
@ | %40 |
% | %25 |
A password of p@ss/w0rd becomes p%40ss%2Fw0rd. Skip it and the driver either throws Password contains unescaped characters straight away, or, worse, silently reads everything after the stray @ as the hostname and reports that it cannot find a server called ss/w0rd.
Do not encode by hand. Every language has the function:
const uri = `mongodb://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@db.example.net:27017/shop?authSource=admin`;from urllib.parse import quote_plusuri = f"mongodb://{quote_plus(user)}:{quote_plus(pw)}@db.example.net:27017/shop?authSource=admin"Better still, keep the credentials out of the string entirely and pass them to the driver as separate arguments, which most drivers support. There is then nothing to encode and nothing to leak into a log line that prints the URI.
mongodb:// or mongodb+srv://#
mongodb+srv:// is not a different protocol. It is a shortcut that tells the driver to look up the host list in DNS instead of reading it from the string. Given mongodb+srv://db.example.net/, the driver queries the SRV record _mongodb._tcp.db.example.net for hostnames and ports, then a TXT record at db.example.net for a small set of default options such as replicaSet and authSource.
Three consequences catch people out:
- You cannot put a port in a
+srvURI. The ports come from the SRV records. - There must be exactly one hostname, and the SRV records have to exist. Pointing
+srvat an ordinary host gives youquerySrv ENOTFOUND _mongodb._tcp.db.example.net. +srvturns TLS on by default. A server that is not configured for TLS will refuse the handshake, and the error rarely mentions TLS.
So: use mongodb+srv:// when your provider gave you a +srv string, which generally means a managed cluster whose membership can change. Use plain mongodb:// with the host and port for a server you rent on its own. If you want a friendly name in front of that host, an A record pointing at the address does the job without any of the SRV machinery - DNS records explained covers which record to use.
authSource, users and roles#
MongoDB users are not global. Every user is created in, and belongs to, a specific database, and the name of that database is what authSource means. The superuser almost always lives in admin, so a URI that names a different default database must say so:
mongodb://root:pw@db.example.net:27017/shop?authSource=adminWithout authSource=admin, the driver looks for a user called root inside shop, does not find one, and fails with Authentication failed and error code 18. The credentials were never wrong. When authSource is not given, it defaults to the database in the URI; when there is no database in the URI either, it defaults to admin.
Do not use the superuser for your application. Make a user scoped to the one database it needs:
use admindb.createUser({ user: "shopapp", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "shop" } ]})Created in admin with a role limited to shop, that user connects with authSource=admin and can do nothing outside shop. Create it in shop instead and the URI uses authSource=shop; both are valid, and picking one convention and sticking to it saves an afternoon later.
The roles worth knowing: read and readWrite for one database, dbAdmin for indexes and statistics, readWriteAnyDatabase for a tool that has to touch everything, and root for the superuser. A backup job needs backup; a monitoring check needs clusterMonitor. Give each connecting thing its own user, because that is what makes the server log and db.currentOp() useful when something misbehaves. The wider version of this argument is in the database security checklist.
Connecting with mongosh and Compass#
mongosh is the current shell; the old mongo binary was removed in MongoDB 6.0. It takes either a full URI or separate flags:
$ mongosh "mongodb://shopapp@db.example.net:27017/shop?authSource=admin"$ mongosh --host db.example.net --port 27017 \ -u shopapp -p --authenticationDatabase admin shopLeave the password off and let it prompt. Typed on the command line it goes into shell history and into the process list, where any other user on the machine can read it.
Once in, four commands tell you where you stand:
db.runCommand({ connectionStatus: 1 }) // who am I, and with what rolesdb.getName() // which database am I inshow dbs // what can I seedb.serverStatus().connections // current, available, totalCreatedmongosh also runs non-interactively, which is what makes it useful in a cron job or a deploy script. mongosh "$MONGODB_URI" --quiet --eval 'db.orders.countDocuments()' prints one number and exits; mongosh "$MONGODB_URI" --file migrate.js runs a file. Both return a non-zero exit status when the script throws, so they compose with set -e the way you would want. Keep those scripts idempotent, because the second time you run one is usually the time it matters.
Compass, the official GUI, takes the same URI in its connection box. It parses it into fields, which makes it a decent way to check whether a string you were given is well formed, and it can save connections as favourites. It also offers an SSH tunnel tab, which is the right answer when the database should not be exposed to the internet at all and you want to reach it through a machine that can. Anything Compass does, mongosh does too, so keep the shell available for the moments when the GUI is confidently wrong.
The connection options worth setting#
| Option | Default | What it does |
|---|---|---|
authSource | the URI database, else admin | Where the user was created |
retryWrites | true | Retries a write once after a network blip |
w | majority on MongoDB 5.0+ | How many members must confirm a write |
readPreference | primary | Where reads go on a replica set |
maxPoolSize | 100 | Connections this client will open per server |
minPoolSize | 0 | Connections kept open when idle |
maxIdleTimeMS | unset | Closes connections idle for longer than this |
serverSelectionTimeoutMS | 30000 | How long to look for a usable server before failing |
connectTimeoutMS | 30000 | TCP connect timeout |
socketTimeoutMS | unset | Leave it alone unless you know why |
appName | none | A label that shows in server logs and currentOp |
tls | false, true with +srv | Encrypts the connection |
compressors | none | zstd, zlib or snappy for the wire protocol |
Three practical notes. maxPoolSize=100 per client is generous; a small application with four worker processes is announcing a possible four hundred connections to a server that may have a few hundred megabytes of memory. Set it to something you have thought about - 10 to 20 per process is plenty for most workloads, and the reasoning generalises in connection pools and limits.
serverSelectionTimeoutMS at its default of thirty seconds is why a misconfigured URI appears to hang rather than to fail. Dropping it to 5000 in development turns a thirty-second mystery into an immediate, readable error.
And appName=checkout-api costs nothing and pays for itself the first time you look at the server log and want to know which of your services issued the query that took nine seconds.
Two options do nothing at all on a standalone server, and it is worth knowing which. readPreference chooses between replica set members, so secondaryPreferred on a single server simply reads from the only server there is. w=majority likewise degrades to "the one node acknowledged it". Neither is harmful, but neither is the durability or the read scaling you might think you bought, and setting them is not a substitute for backups. The real durability control on a standalone instance is journalling, which is on by default and flushes on a short interval, and the real read-scaling control is an index that stops the query reading a million documents.
Drivers: one client, reused#
The mistake that produces most MongoDB performance complaints is creating a client per request. A MongoClient is a pool, a topology monitor and a set of background threads. It is designed to be created once when the process starts and shared for the life of the process. It is thread-safe and safe to share across async tasks; it is not safe to share across a fork.
import { MongoClient } from "mongodb";const client = new MongoClient(process.env.MONGODB_URI, { maxPoolSize: 20, serverSelectionTimeoutMS: 5000, appName: "checkout-api",});await client.connect();const shop = client.db("shop");export const orders = shop.collection("orders");from pymongo import MongoClientclient = MongoClient( os.environ["MONGODB_URI"], maxPoolSize=20, serverSelectionTimeoutMS=5000, appName="checkout-api",)orders = client["shop"]["orders"]Both clients connect lazily: nothing happens on the network until the first operation, so a URI that is wrong can look fine at startup and fail on the first request. If you want to know at boot, issue a ping as part of your health check:
await client.db("admin").command({ ping: 1 });That is also the right body for a readiness probe, because it proves the pool can reach a server and authenticate, which is what "is the database up" actually means.
Keeping the string out of your repository#
The URI contains a password, so it is a secret and belongs in the environment, not in the source tree:
MONGODB_URI=mongodb://shopapp:p%40ss@db.example.net:27017/shop?authSource=adminFour rules that follow from that, and they are boring for a reason. Keep .env in .gitignore and commit an .env.example with the shape but no values. Use a different user and a different database for staging than for production, so a misconfigured staging deploy cannot write to real data. Log the URI with the credentials stripped, or not at all, because an error handler that prints the connection string puts the password into your log aggregator forever. And rotate the password when someone leaves, which is only realistic if the application reads it from one place. Environment variables and secrets goes through where those values should live.
Exposure matters as much as the password. A database reachable from the whole internet with a guessable user is found by scanners in hours, and the ransom-note collections people discover in unauthenticated MongoDB instances are not a myth. Make sure authentication is actually enabled, restrict where connections may come from if you can, and never leave a test instance running with the default configuration.
Errors and what they actually mean#
`Authentication failed` (code 18) - wrong authSource far more often than a wrong password. Check which database the user was created in, then try the same credentials in mongosh with --authenticationDatabase.
`MongoServerSelectionError` / `ServerSelectionTimeoutError` - the driver could not find a server to talk to within serverSelectionTimeoutMS. Nothing to do with credentials. Wrong host, wrong port, the server not listening on a reachable address, or a firewall.
`connect ECONNREFUSED 203.0.113.10:27017` - something answered at that address and refused. Usually the server is bound only to 127.0.0.1, which has been the default since MongoDB 3.6.
`querySrv ENOTFOUND _mongodb._tcp.example.net` - a +srv URI pointing at a host with no SRV records. Use mongodb:// with the port.
`Password contains unescaped characters` - percent-encode, as above.
`command find requires authentication` - you are connected but anonymous. The credentials were not in the URI, or the driver was given a database that is not the authSource.
`Unsupported OP_QUERY command` - the driver is older than the server and is speaking a protocol removed in MongoDB 5.1. Upgrade the driver package.
FAQ#
What is authSource and why do I need it?
It names the database that holds the user account, which is not necessarily the database you want to work in. Superusers usually live in admin, so a URI pointing at an application database needs authSource=admin to find them. Get it wrong and you see an authentication failure with perfectly correct credentials.
What is the difference between mongodb:// and mongodb+srv://?
mongodb+srv:// asks DNS for the list of servers instead of reading it from the URI, and turns TLS on by default. It needs SRV records to exist and cannot include a port. For a single standalone server, plain mongodb:// with the host and port is the right form.
Can I connect to MongoDB from a browser?
No. MongoDB speaks a binary wire protocol over TCP, not HTTP, so the connection is made by your server-side code. That is also why a database plan has no reverse proxy in front of it: there would be nothing for it to proxy. Your application connects on the port, and the browser talks to your application.
Do I need TLS on my connection string?
If the traffic crosses the public internet, yes, and the server has to be configured for it first. Inside one machine, where the application and the database are on the same host and the database only listens on the loopback address, it adds nothing. Turning tls=true on against a server that is not set up for it fails at the handshake.
How many connections should my application open?
Far fewer than the default of 100 per client. Start at 10 to 20 per process, multiply by the number of processes, and check the result against db.serverStatus().connections. Each open connection costs memory on the server, so a pool sized by hope is a way to run a small instance out of it.
Where do I go next?
Once you are connected, the two things that decide whether the database stays fast are how documents are shaped and which indexes exist - see MongoDB schema design and indexes. Before you put anything real in it, read mongodump and mongorestore and take a backup you have restored at least once.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.