MP-301c · Module 1

Connection Pooling & Resource Management

3 min read

Database-backed MCP tools that create a new connection per tool call will collapse under moderate load. Connection creation takes 50-200ms for PostgreSQL, 10-50ms for Redis, and involves TLS handshakes for cloud-hosted databases. Connection pooling amortizes this cost by maintaining a pool of pre-established connections and lending them to tool calls on demand. The tool handler borrows a connection, executes its query, and returns the connection to the pool — the next tool call gets the same connection without creation overhead.

Pool sizing is a tuning exercise. Too few connections and tool calls queue up waiting for a free connection. Too many and you overwhelm the database server with concurrent queries. The formula: start with pool_size = number_of_CPU_cores * 2 + 1 for the database server (not your MCP server), then measure. If pool wait time is consistently above 10ms, increase the pool. If the database CPU is consistently above 80%, decrease the pool. Monitor both metrics continuously — the optimal pool size changes with load patterns.

import pg from "pg";

// Singleton connection pool — shared across all tool handlers
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,                  // Max connections in pool
  idleTimeoutMillis: 30_000, // Close idle connections after 30s
  connectionTimeoutMillis: 5_000, // Fail if can't connect in 5s
});

// Log pool events for monitoring
pool.on("error", (err) => {
  console.error(JSON.stringify({ event: "pool_error", error: err.message }));
});

pool.on("connect", () => {
  console.error(JSON.stringify({
    event: "pool_connect",
    total: pool.totalCount,
    idle: pool.idleCount,
    waiting: pool.waitingCount,
  }));
});

// Resource-safe query wrapper
export async function query<T>(sql: string, params?: unknown[]): Promise<T[]> {
  const client = await pool.connect();
  try {
    const result = await client.query(sql, params);
    return result.rows as T[];
  } finally {
    client.release(); // ALWAYS release back to pool
  }
}

// Graceful shutdown
process.on("SIGTERM", async () => {
  console.error(JSON.stringify({ event: "pool_shutdown" }));
  await pool.end();
  process.exit(0);
});
  1. Create a singleton pool Initialize the connection pool once at server startup. Export a query wrapper that borrows, uses, and releases connections. Never expose the pool directly to handlers.
  2. Set timeouts aggressively Connection timeout: 5s (fail fast if DB is down). Idle timeout: 30s (release unused connections). Query timeout: match your tool's latency budget.
  3. Monitor pool utilization Log total, idle, and waiting counts on every connection event. Alert when waiting > 0 for sustained periods — this means your pool is too small or your queries are too slow.