How to Connect to MongoDB from Go
You copy a connection string from a dashboard. You paste it into a Go file. You run the program and it prints "Connected!" before crashing five seconds later with a timeout. Or it hangs silently while the database rejects the handshake. Connecting to MongoDB in Go requires more than a URI. The official driver handles lazy initialization, connection pooling, and background monitoring. Treat the client like a synchronous socket and you will spend your afternoon debugging pool exhaustion instead of writing features.
The client is a pool, not a socket
The mongo.Client is a handle to a connection pool, not a single active socket. Think of it like a gym membership card. Showing the card at the front desk does not put you on a treadmill. It grants you permission to enter, and the facility manages the equipment for you. When you create the client, Go allocates the pool and prepares to talk to the server. The actual network handshake happens when you first execute a query or explicitly ping the database. This lazy behavior saves resources if the client is created but never used. It also means your startup sequence can look successful while the network is completely down.
The connection string carries authentication credentials, TLS settings, replica set names, and load balancer hints. The driver parses this URI and builds a configuration object. If you use the mongodb+srv:// scheme, the driver performs DNS SRV lookups to discover the cluster topology. This is standard for managed services like MongoDB Atlas. The driver handles service discovery automatically. You do not need to write custom DNS logic.
The client is safe for concurrent use by multiple goroutines. Create one client per application instance and share it everywhere. Do not create a new client per request. That destroys the connection pool benefits and exhausts file descriptors. The pool manages the lifecycle of individual connections. It opens new connections when demand rises and closes idle ones when demand drops. You control the pool size and timeouts via options, but the driver handles the bookkeeping.
The client is a pool, not a socket. Ping it or you are guessing.
Minimal connection with validation
Here is the bare minimum to spin up a client and verify the link is alive. The code uses a context with a timeout to prevent hanging, and it calls Ping to force the handshake during initialization.
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
// Context carries timeout and cancellation signals to the driver.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// ApplyURI parses the connection string and sets default options.
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatalf("failed to initialize client: %v", err)
}
// Ping forces the driver to perform the actual handshake now.
err = client.Ping(ctx, nil)
if err != nil {
log.Fatalf("failed to ping database: %v", err)
}
log.Println("Connected to MongoDB")
}
What happens under the hood
When you call mongo.Connect, the driver parses the URI. It extracts the host, port, authentication mechanism, and TLS settings. It builds a configuration object but does not open sockets yet. The Connect function returns immediately if the URI syntax is valid. If you skip the Ping, the program proceeds assuming the database is reachable. The first read or write operation triggers the connection attempt. If the server is down, that operation fails with a network error. Calling Ping moves the failure point to initialization. This is usually what you want in a service startup sequence. You fail fast rather than crashing in the middle of a request.
The context passed to Connect and Ping sets a deadline. If the handshake takes longer than the deadline, the driver cancels the operation and returns an error. Always use a context with a timeout for connection steps. An infinite hang during startup is a production nightmare. The Ping method sends an ismaster command to the server. This verifies that the server is responsive, that authentication succeeds, and that the driver can negotiate the protocol version. If the server rejects the ping, you get an error immediately.
The driver also starts a background monitor goroutine. This monitor periodically checks the server status and updates the internal view of the cluster topology. If you are connecting to a replica set, the monitor detects primary elections and failovers. Your application code does not need to handle these events. The driver routes requests to the correct node automatically. The monitor respects the context cancellation. When you call Disconnect, the monitor stops and the pool drains.
Every database method in the Go driver takes a context.Context as the first parameter. The community convention is to name it ctx. Functions that accept a context should respect cancellation and deadlines. Pass a context with a timeout to every long-lived call site.
Ping moves the failure to startup. Fail fast.
Realistic application setup
In a real application, you do not create the client in main and pass it around as a global variable. You wrap it in a struct and inject it where needed. The client is safe for concurrent use by multiple goroutines. You create one client per application instance and share it. Here is the initialization logic. Wrap this in a constructor function.
// App holds the shared database client.
type App struct {
client *mongo.Client
}
// NewApp connects to MongoDB and validates the link.
func NewApp(ctx context.Context, uri string) (*App, error) {
// Timeout prevents hanging during startup if the DB is unreachable.
connCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// ApplyURI configures the client from the connection string.
client, err := mongo.Connect(connCtx, options.Client().ApplyURI(uri))
if err != nil {
return nil, err
}
// Ping forces the handshake to happen during initialization.
if err := client.Ping(connCtx, nil); err != nil {
return nil, err
}
return &App{client: client}, nil
}
The client is embedded in the App struct. Other parts of the application receive the App instance and access the client through methods. This pattern makes testing easier. You can mock the database layer or swap the client for a test instance. The driver follows Go conventions strictly. Export the struct if it crosses package boundaries. Keep the receiver name short and matching the type, like (a *App). Do not use this or self. The community accepts verbose error handling because it makes the unhappy path visible. Check every error immediately and return it.
Share the client. Leak the connection, leak the process.
Tuning the connection pool
The driver defaults are good for most applications. The default maximum pool size is 100 connections. If you have high concurrency, you might need more. Use options.Client().SetMaxPoolSize to increase the limit. Do not set it to infinity. The database has limits. Too many connections cause thrashing and increase memory usage on the server. Monitor the pool stats to find the right balance. The driver exposes metrics via client.PoolStats(). You can log these metrics to track active connections, wait queue length, and creation time.
Here is how to tune the pool for a high-throughput service. The options builder lets you chain settings.
// NewTunedClient creates a client with explicit pool and timeout settings.
func NewTunedClient(ctx context.Context, uri string) (*mongo.Client, error) {
// Server selection timeout controls how long the driver waits for a primary.
opts := options.Client().
ApplyURI(uri).
SetServerSelectionTimeout(5 * time.Second).
SetMaxPoolSize(200).
SetMinPoolSize(10)
// Connect uses the tuned options.
client, err := mongo.Connect(ctx, opts)
if err != nil {
return nil, err
}
// Validate the connection.
if err := client.Ping(ctx, nil); err != nil {
return nil, err
}
return client, nil
}
SetMinPoolSize keeps a baseline of connections open. This reduces latency for the first few requests after a quiet period. SetServerSelectionTimeout limits how long the driver waits to find a suitable server. If the cluster is unhealthy, this timeout prevents the driver from blocking indefinitely. Defaults work for most apps. Tune only when metrics demand it.
Defaults work for most apps. Tune only when metrics demand it.
Pitfalls and error handling
The most common failure is a timeout during server selection. If the URI points to a host that does not respond, the driver retries until the context expires. The runtime will reject the operation with server selection error: context deadline exceeded. This usually means the host is down, the port is wrong, or a firewall is blocking the connection. Check the URI and network rules. Another trap is authentication. If credentials are missing or wrong, the driver returns auth error: sasl conversation error. The driver does not fail immediately on bad credentials. It tries to connect, then fails during the auth handshake. Always include credentials in the URI or options for production.
A subtle bug is forgetting to close the client. The client holds a connection pool. If you call mongo.Connect in a loop or a test without calling Disconnect, you leak connections. The pool grows until the system runs out of file descriptors. Call client.Disconnect(ctx) when the application shuts down. Use defer in main or a shutdown hook in a server. The disconnect operation also cancels the background monitor. Goroutine leaks happen when the goroutine waits on a channel that never gets closed. Always have a cancellation path.
Context propagation is critical. If you pass context.Background() to a query, the query runs until it completes or the server kills it. If a user cancels an HTTP request, the handler should cancel the context passed to the DB query. This stops the query on the server side and releases the connection back to the pool immediately. If you ignore cancellation, the connection sits idle until the query finishes or times out, wasting resources. The driver provides helper functions to classify errors. Use mongo.IsTimeout(err), mongo.IsNetworkError(err), and mongo.IsRetryableError(err) to decide whether to retry an operation. Retryable writes are supported automatically for most operations. The driver retries transparently on transient network errors.
Timeouts are symptoms. Check the URI and the firewall.
When to use this approach
Use mongo.Connect with Ping when you need to validate the database link during startup. Use a shared *mongo.Client instance when multiple handlers or goroutines access the database. Use context.WithTimeout when initializing the client to prevent the application from hanging indefinitely. Use client.Disconnect when the application exits to release the connection pool. Use a separate client per database cluster when you need to connect to multiple distinct MongoDB deployments. Use environment variables for the connection string when deploying to different environments. Use the official go.mongodb.org/mongo-driver package when you need full feature support and active maintenance. Use an ORM like GORM only when you prefer a higher-level abstraction and accept the performance overhead.
One client per cluster. Ping at startup. Disconnect at shutdown.