ruvnet/ruflo · error

Connection pool is shutting down

Error message

Connection pool is shutting down

What it means

The MCP connection pool's acquire() refuses to hand out a connection once shutdown() has begun: during teardown idle connections are destroyed and the pool drains, so any handle granted mid-shutdown would be dead on arrival. The internal isShuttingDown flag is checked at the top of acquire(), making the race fail fast instead of returning a broken connection. Note isShuttingDown is private; pool.isHealthy() indirectly reflects it (it returns false when shutting down).

Source

Thrown at v3/@claude-flow/shared/src/mcp/connection-pool.ts:169

    const connection = new ManagedConnection(id, this.transportType);

    this.connections.set(id, connection);
    this.stats.totalCreated++;

    this.emit('pool:connection:created', { connectionId: id });
    this.logger.debug('Connection created', { id, total: this.connections.size });

    return connection;
  }

  /**
   * Acquire a connection from the pool
   */
  async acquire(): Promise<PooledConnection> {
    const startTime = performance.now();

    if (this.isShuttingDown) {
      throw new Error('Connection pool is shutting down');
    }

    // Try to find an idle connection
    for (const connection of this.connections.values()) {
      if (connection.state === 'idle' && connection.isHealthy()) {
        connection.acquire();
        this.stats.totalAcquired++;
        this.recordAcquireTime(startTime);

        this.emit('pool:connection:acquired', { connectionId: connection.id });
        this.logger.debug('Connection acquired from pool', { id: connection.id });

        return connection;
      }
    }

    // Create new connection if under limit
    if (this.connections.size < this.config.maxConnections) {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Fix teardown order: stop admitting new work (close the HTTP server, set a draining flag), await outstanding requests, THEN call pool.shutdown()
  2. Gate every acquire with a caller-side closing flag, and treat 'Connection pool is shutting down' as a terminal, non-retryable signal
  3. Await pool.shutdown() fully during teardown so no acquire can race it
  4. In tests, ensure every request has settled before closing a shared pool in afterAll

Example fix

// before
process.on('SIGTERM', () => pool.shutdown());
app.listen(3000); // requests still arrive; acquire() races shutdown

// after
process.on('SIGTERM', async () => {
  draining = true;                  // gate new acquires in the request path
  await closeServerGracefully();    // stop admitting work first
  await pool.shutdown();            // now drain safely
});
Defensive patterns

Strategy: try-catch

Validate before calling

if (draining || !pool.isHealthy()) {
  throw new ServiceUnavailableError('pool is draining');
}
const conn = await pool.acquire(); // still wrap in try-catch: isHealthy() can race shutdown

Try / catch

try {
  return await withTimeout(pool.acquire(), 5_000);
} catch (e) {
  if (e instanceof Error && /shutting down/i.test(e.message)) {
    throw new ServiceUnavailableError('MCP pool is shutting down'); // terminal: never retry
  }
  throw e; // other errors may be transient and handled by the caller
}

Prevention

When it happens

Trigger: An in-flight request calls acquire() after a SIGTERM handler invoked pool.shutdown(); a background loop (heartbeat, keepalive, worker poll) keeps acquiring while the app closes; the HTTP server still accepts new work after pool.shutdown() was called; a test's afterAll closes a shared pool while a dangling async test still acquires.

Common situations: Graceful-shutdown ordering bugs: closing the pool before the listener that feeds it; long-lived workers never signalled to stop before teardown; retry wrappers that treat this terminal error as transient and keep looping.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/9743fe654fd4dfaf. Report an issue: GitHub.