MP-301d · Module 3
Connection Pooling
3 min read
Every MCP resource read that hits the database needs a connection. Opening a new connection per request — TCP handshake, TLS negotiation, authentication — adds 50-200ms of latency and creates connection churn that databases handle poorly. Connection pooling solves this: a pool of pre-established connections is maintained by the MCP server, and each resource read borrows a connection, executes its query, and returns it to the pool. The connection stays open between reads, eliminating setup overhead.
Pool sizing is the critical tuning parameter. Too few connections and resource reads queue up, increasing latency. Too many and you overwhelm the database with concurrent queries, consuming memory and CPU. The right size depends on your database — PostgreSQL defaults to 100 max connections, shared across all applications. A single MCP server should claim a small fraction: 5-20 connections is typical. Set min to 2 (always ready), max to 10-20 (burst capacity), and idleTimeoutMillis to 30 seconds (release unused connections).
For production deployments, consider an external connection pooler like PgBouncer or PgCat. These sit between the MCP server and the database, multiplexing many application connections over fewer database connections. This is essential when running multiple MCP server instances behind a load balancer — without a pooler, each instance maintains its own pool, and the total connection count quickly exceeds the database limit.
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
min: 2, // Always keep 2 connections warm
max: 10, // Burst up to 10 concurrent queries
idleTimeoutMillis: 30_000, // Release idle connections after 30s
connectionTimeoutMillis: 5_000, // Fail fast if pool is exhausted
statement_timeout: 10_000, // Kill queries running longer than 10s
});
// Health check resource — expose pool status
server.resource("pool-health", "db://health/pool", async (uri) => {
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
maxConnections: 10,
utilizationPct: Math.round(
((pool.totalCount - pool.idleCount) / 10) * 100
),
}),
}],
};
});
// Graceful shutdown
process.on("SIGTERM", async () => {
await pool.end();
process.exit(0);
});
- Configure the pool Set min=2, max=10, idleTimeout=30s, connectionTimeout=5s. Adjust max based on your database connection limit and the number of MCP server instances.
- Add statement timeout Set a statement_timeout (10-30s) to kill runaway queries. An AI model requesting a table scan on a billion-row table should not hold a connection for minutes.
- Monitor utilization Expose pool metrics as a health resource. Alert when waitingCount is consistently above zero — that means reads are queuing for connections.