PostgreSQL接続プール枯渇の対処法|too many connections・PgBouncer・Node.js pg

(更新: 2026年6月21日 ) PostgreSQL PgBouncer Node.js バックエンド インフラ 運用
結論
  • connection pool exhausted は「接続を借りたまま返していない」「プール上限 × インスタンス数が PostgreSQL の max_connections を超えた」「スパイク時にプール待ちタイムアウトが短い」のいずれかが多い。
  • 対処の定石は (1) pg_stat_activity で idle in transaction を潰す(2) アプリ pool.max を (max_connections - 予約) / インスタンス数 に収める(3) PgBouncer で transaction pooling する の3点。
  • Node.js では pool.query() を基本とし、pool.connect() は必ず finally { client.release() } する。

connection pool exhausted とは

本番で突然 API が 500/503 を返し始め、ログに次のようなメッセージが並ぶ——これが接続プール枯渇の典型像です。

Error: timeout exceeded when trying to connect
Error: Connection pool exhausted
FATAL: sorry, too many clients already
FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute

connection pool exhausted は、アプリケーション(または PgBouncer)が管理する接続プールの空きスロットがなくなり、新規クエリが待ち行列に入るか拒否される状態を指します。PostgreSQL サーバー本体の max_connections に達した場合は too many connections として FATAL エラーが返ります。

どちらも「DB に新しい接続を確立できない」という結果は同じですが、発生レイヤーが異なります

レイヤー 症状・典型原因
アプリ側プール(pg.Pool 等) timeout exceeded when trying to connect。pool.max に達し、release 漏れや長時間クエリでスロットが埋まる
PgBouncer no more connections allowed (max_client_conn)。クライアント接続は多いが DB 側はプールサイズ分だけ。設定ミスで詰まることも
PostgreSQL 本体 sorry, too many clients already。全接続の合計が max_connections 超過。スーパーユーザー用の reserved_connections 以外は拒否

枯渇はピーク時だけでなく、接続リークがあると数時間かけてゆっくり進行し、深夜のバッチ開始と重なって初めて表面化することもあります。

PostgreSQL 接続数上限の仕組み

PostgreSQL はプロセスモデルです。クライアント接続1本につき、通常は専用のバックエンドプロセス(postgres ワーカー)が1つ割り当てられます。接続数が増えるほどメモリ消費とコンテキストスイッチが増え、OS のファイルディスクリプタ上限にも触れます。

max_connections と reserved_connections

postgresql.conf(または ALTER SYSTEM)で決まる主要パラメータです。

パラメータデフォルト意味
max_connections100同時接続のハード上限
superuser_reserved_connections3スーパーユーザー専用の予約スロット

一般ロールが使える接続数は実質 max_connections - superuser_reserved_connections です。100 設定なら、通常ユーザーは 97 本まで。

現在値の確認:

SHOW max_connections;
SHOW superuser_reserved_connections;

SELECT
  count(*) AS total_connections,
  count(*) FILTER (WHERE state = 'active') AS active,
  count(*) FILTER (WHERE state = 'idle') AS idle,
  count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx
FROM pg_stat_activity
WHERE backend_type = 'client backend';

接続数の「予算」計算

本番設計では、次の式でアプリに割り当て可能な接続数を逆算します。

アプリ1台あたり pool.max ≤ floor(
  (max_connections - superuser_reserved - 管理・レプリケーション・バッチ予約)
  / アプリインスタンス数
)

例: max_connections = 200、予約 20(レプリケーション・監視・マイグレーション)、アプリ 4 台

(200 - 20) / 4 = 45 → pool.max は 40〜45 程度

PgBouncer ありの場合、PostgreSQL には PgBouncer の server 接続合計だけが届きます。

PostgreSQL 側 ≈ sum(PgBouncer の default_pool_size × データベース数) + 管理接続

アプリ 100 台 × pool.max = 10 でも、PgBouncer default_pool_size = 25 なら DB には 25 本程度——これが PgBouncer の核心です。

なぜ枯渇するのか:5つの典型原因

1

[object Object]

2

[object Object]

3

[object Object]

4

[object Object]

5

[object Object]

原因1: 接続リーク(最も多い)

pool.connect() で借りた clientrelease() しないと、そのスロットはプロセスが死ぬまでプールから消えません。リクエスト 1 万本処理すれば、理論上 1 万本の接続がプール上限を食い尽くします(実際は max に達した時点で停止)。

リークは例外パスで顕在化します。

// ❌ 危険: クエリ失敗時に release されない
const client = await pool.connect();
const res = await client.query('SELECT ...'); // ここで throw
client.release(); // 到達しない

原因2: 水平スケール時の積み上げ

オートスケールで Pod 数が増えると、各 Pod が独立したプールを持ちます。

構成pool.maxインスタンス合計要求
開発10110
ステージング20240
本番(平常)20480
本番(スケールアウト)2020400

max_connections = 100 の RDS では、スケールアウトだけで上限超過します。PgBouncer なしのまま Pod を増やすのは典型的な設計ミスです。

原因3: idle in transaction

トランザクションを開いたまま外部 API を待つと、接続は idle in transaction 状態で占有されます。

SELECT pid, state, wait_event_type, wait_event,
       now() - xact_start AS tx_duration,
       left(query, 80) AS query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;

1 本 30 秒の外部 API × 同時 100 リクエスト = 100 接続が 30 秒ブロック——これだけで枯渇します。

原因4: 接続確立コストの無視

プールなしでリクエストごとに new Client().connect() すると、TLS + SCRAM 認証で 50〜200ms かかることもあります。スパイク時に接続確立が追いつかず、既存接続も長時間クエリで塞がって「プール exhausted」に見えます。

原因5: ワーカー数とプールサイズ

Node.js 単一プロセスなら pool.max = 10〜20 が一般的です。Cluster モードや PM2 instances: max ではプロセス数 × pool.max になります。

PM2 instances = 8, pool.max = 20 → 1 台のサーバーで 160 接続要求

監視クエリ:今誰が接続を握っているか

障害時の第一アクションは pg_stat_activity です。以下を psql または監視 DB から実行します。

接続数サマリー(データベース別)

SELECT
  datname,
  numbackends AS connections,
  xact_commit,
  xact_rollback,
  blks_hit,
  blks_read
FROM pg_stat_database
WHERE datname IS NOT NULL
ORDER BY numbackends DESC;

アプリケーション名・ユーザー別

SELECT
  usename,
  application_name,
  client_addr,
  count(*) AS conn_count,
  count(*) FILTER (WHERE state = 'active') AS active,
  count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY 1, 2, 3
ORDER BY conn_count DESC;

application_name は Node.js 側で設定できます(後述)。どのデプロイ単位が接続を食っているかが一目で分かります。

長時間クエリ・ブロッキング

SELECT
  blocked.pid AS blocked_pid,
  blocked.usename AS blocked_user,
  blocking.pid AS blocking_pid,
  blocking.usename AS blocking_user,
  left(blocked.query, 60) AS blocked_query,
  now() - blocked.query_start AS blocked_duration
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';

接続上限に対する使用率

SELECT
  (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend') AS current,
  (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn,
  round(
    100.0 * (SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend')
    / (SELECT setting::int FROM pg_settings WHERE name = 'max_connections'),
    1
  ) AS usage_pct;

usage_pct が 80% を超えたら警告、90% でページング——という閾値が実務的です。

接続リーク疑いのセッション特定

同じ application_name + client_addr から idle が異常に多い場合:

SELECT
  pid,
  application_name,
  client_addr,
  state,
  state_change,
  now() - state_change AS idle_duration,
  query
FROM pg_stat_activity
WHERE state = 'idle'
  AND backend_type = 'client backend'
  AND now() - state_change > interval '10 minutes'
ORDER BY idle_duration DESC;

10 分以上 idle のまま返却されていない接続は、プールの idleTimeoutMillis より長い可能性があり、別プロセスのリークまたは PgBouncer のクライアント側滞留を疑います。

Node.js pg:Pool 設定の実務

node-postgrespg)の Pool は、接続の再利用と同時実行上限を担います。

推奨設定例(TypeScript)

import { Pool, PoolConfig, QueryResultRow } from 'pg';

const poolConfig: PoolConfig = {
  connectionString: process.env.DATABASE_URL,
  // 同時に PostgreSQL へ開く接続の上限(1 Node プロセスあたり)
  max: parseInt(process.env.PG_POOL_MAX ?? '20', 10),
  // 最小アイドル接続(0 なら需要時のみ作成)
  min: parseInt(process.env.PG_POOL_MIN ?? '2', 10),
  // プールから接続を得るまでの待ち時間(ms)
  connectionTimeoutMillis: 5_000,
  // アイドル接続を閉じるまでの時間
  idleTimeoutMillis: 30_000,
  // クエリ実行のタイムアウト(pg 8.11+)
  query_timeout: 30_000,
  // サーバー側にも application_name を渡す(監視用)
  application_name: `api-${process.env.HOSTNAME ?? 'local'}`,
  // SSL(RDS 等)
  ssl: process.env.PG_SSL === 'true'
    ? { rejectUnauthorized: true }
    : undefined,
};

export const pool = new Pool(poolConfig);

// プールレベルのエラー(アイドル接続の突然の切断等)
pool.on('error', (err) => {
  console.error('[pg pool] unexpected error on idle client', err);
});

pool.max の決め方

要素目安
CPU バウンドな短い APImax = 10〜20
重い JOIN / レポートmax = 5〜10、別読み取り専用プール
PgBouncer transaction modemax はやや大きくても DB 負荷は抑えられる
PM2 cluster 8 プロセス上記を 8 で割った値を 1 プロセスあたりに

「CPU コア数 × 2」は PostgreSQL 接続数の公式ではない——I/O 待ちが多い Web API ではコア数より多い接続が有効なことも、逆に少ない方が速いこともあります。メトリクスで調整します。

安全なクエリ実行パターン

原則: pool.query() を使う——内部で connect → query → release まで面倒を見てくれます。

export async function findUserById(id: string): Promise<User | null> {
  const { rows } = await pool.query<UserRow>(
    'SELECT id, email, name FROM users WHERE id = $1',
    [id],
  );
  return rows[0] ?? null;
}

トランザクション:connect + try/finally

複数クエリを 1 トランザクションにまとめる場合だけ connect() します。

import { PoolClient } from 'pg';
import { pool } from './db';

export async function transferCredits(
  fromId: string,
  toId: string,
  amount: number,
): Promise<void> {
  const client: PoolClient = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query(
      'UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1',
      [amount, fromId],
    );
    await client.query(
      'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
      [amount, toId],
    );
    await client.query('COMMIT');
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release(); // 成功・失敗・return を問わず必須
  }
}
release() を忘れると確実に枯渇する

finally { client.release() }絶対規則です。ESLint カスタムルールや using(Explicit Resource Management)で強制するチームも増えています。トランザクション内で return しても finally は実行されるため、ここに release を置くのが最も安全です。

リトライと acquire タイムアウト

一時的な枯渇時、即 500 を返すより短いリトライが有効な場合があります。

async function queryWithRetry<T extends QueryResultRow>(
  text: string,
  params: unknown[],
  maxAttempts = 3,
): Promise<T[]> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const { rows } = await pool.query<T>(text, params);
      return rows;
    } catch (err) {
      lastError = err;
      const msg = err instanceof Error ? err.message : String(err);
      const retryable =
        msg.includes('timeout exceeded when trying to connect') ||
        msg.includes('Connection terminated unexpectedly');
      if (!retryable || attempt === maxAttempts) break;
      await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
    }
  }
  throw lastError;
}

恒常的な枯渇をリトライで隠さないこと——アラートは別途必須です。

Graceful shutdown

デプロイ・Pod 再起動時に接続を適切に閉じます。

async function shutdown(signal: string): Promise<void> {
  console.log(`Received ${signal}, closing pool...`);
  await pool.end(); // 進行中クエリ完了後に全接続を閉じる
  process.exit(0);
}

process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));

K8s では terminationGracePeriodSecondsquery_timeout + バッファより長く取り、SIGTERM 後もクエリが完了する時間を確保します。

サーバー側タイムアウト(postgresql.conf)

アプリだけでは防げない長時間トランザクションを DB 側で切ります。

-- セッション単位(ロール別に ALTER ROLE でも可)
SET statement_timeout = '30s';
SET idle_in_transaction_session_timeout = '60s';

idle_in_transaction_session_timeout は PostgreSQL 14+ で特に重要——放置トランザクションを強制終了し、接続を解放します。

PgBouncer 設定:ini ファイルの実例

PgBouncer は軽量な接続プーラーで、PostgreSQL の前段に置きます。

最小構成(pgbouncer.ini)

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

; クライアント(アプリ)から PgBouncer への同時接続上限
max_client_conn = 1000

; PostgreSQL へ張る接続プールサイズ(DB ごと)
default_pool_size = 25

; トランザクション単位で接続を返却(Web API 向け)
pool_mode = transaction

; サーバー接続の最小・最大
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3

; クエリ・ログ
server_reset_query = DISCARD ALL
server_check_query = SELECT 1
server_check_delay = 30

; タイムアウト(秒)
query_timeout = 30
query_wait_timeout = 120
client_idle_timeout = 300

log_connections = 1
log_disconnections = 1
stats_period = 60

admin_users = pgbouncer_admin

アプリの DATABASE_URL は PostgreSQL 直ではなく postgres://user:pass@pgbouncer-host:6432/appdb に向けます。

pool_mode の選び方

モード 挙動・向き不向き
session クライアント接続中は常に同一 PostgreSQL セッション。Prepared statement・SET・LISTEN/NOTIFY・一時テーブルが使える。DB 接続数削減効果は小さい
transaction トランザクション終了後に接続を返却。Web API の短い read/write に最適。DISCARD ALL 必須。セッション跨ぎの SET に注意
statement クエリ単位で返却。最も aggressive。トランザクションをまたぐ処理は不可。特殊用途

Node.js pg + 通常の REST API なら transaction pooling が第一選択です。

userlist.txt の例

"app_user" "SCRAM-SHA-256$4096:..."

パスワードハッシュは echo -n 'passwordapp_user' | md5sum ではなく、PostgreSQL 14+ の SCRAM 形式を使います。

PgBouncer 管理コンソール

psql -h 127.0.0.1 -p 6432 -U pgbouncer_admin pgbouncer
SHOW POOLS;
SHOW CLIENTS;
SHOW SERVERS;
SHOW STATS;

SHOW POOLScl_active / cl_waiting / sv_active を監視します。cl_waiting が継続的に > 0 なら、プールサイズ不足または DB 側の遅延クエリが原因です。

カラム意味
cl_activeクライアントがクエリ実行中
cl_waitingプールの空き待ち(要警戒)
sv_activePostgreSQL 側で実行中
sv_idleプール内アイドル server 接続

接続リーク検知パターン

パターン1: プールメトリクスの異常

pg プールは内部状態を直接 Prometheus に出しませんが、ラッパーで観測できます。

import { pool } from './db';

export function getPoolStats() {
  return {
    total: pool.totalCount,
    idle: pool.idleCount,
    waiting: pool.waitingCount,
  };
}

// /health/detailed 等で露出
// waiting > 0 が数十秒続く → ページング
メトリクス正常時異常時
waitingCount0 が大半スパイク時のみ短時間
totalCountmax 未満常に max = リークまたは過負荷
idleCount変動あり0 固定 + waiting > 0

パターン2: pg_stat_activity の application_name 監視

デプロイごとに application_name を変え、接続数の偏りを検知します。

application_name: `api-${process.env.K8S_POD_NAME ?? 'local'}`,

Grafana で count by (application_name) をプロットし、特定 Pod だけ接続が増え続ける場合、その Pod だけリークしている可能性が高いです。

パターン3: コードレビュー grep チェックリスト

# connect しているのに release がファイル内にない疑い
rg "pool\.connect\(\)" --type ts -l | while read f; do
  if ! rg -q "\.release\(\)" "$f"; then echo "MISSING release: $f"; fi
done

# new Client() の直接使用(プールバイパス)
rg "new Client\(" --type ts

パターン4: 長寿命イベントハンドラ

// ❌ WebSocket セッション全体で 1 client を保持
const client = await pool.connect();
ws.on('message', async (msg) => {
  await client.query('INSERT INTO events ...', [msg]);
});
// ws が閉じるまで release されない → 同時 WS 数 = 接続数

WebSocket / SSE ではメッセージごとに pool.query するか、専用のメッセージキュー(Redis / SQS)に逃がします。

パターン5: ORM の落とし穴

Prisma / TypeORM / Sequelize も結局はプールの上に乗ります。

  • Prisma: connection_limit を datasource URL で指定。Serverless では Prisma Accelerate / Data Proxy や PgBouncer 必須
  • TypeORM: extra.max が pg の max に相当。QueryRunnerrelease() 必須
  • マイグレーション: デプロイ時に全インスタンスが同時 migrate しないよう、Job を 1 本に限定

アーキテクチャ別ベストプラクティス

単一 Node サーバー + PostgreSQL 直

  • pool.max = 20 前後から開始
  • idle_in_transaction_session_timeout を必ず設定
  • 接続数ダッシュボードを 1 枚用意

K8s 複数 Pod + PgBouncer

[Pod × N] --pool.max=30--> [PgBouncer :6432] --default_pool_size=40--> [PostgreSQL :5432]
  • Pod 側 pool.max は大きめでも可(PgBouncer が絞る)
  • PgBouncer は Deployment 単位で HA(2 台 + Service)または Sidecar
  • Read replica には 別 PgBouncer プールまたは pool 読み取り専用 URL

Serverless(Lambda / Cloud Functions)

接続確立が遅く、同時実行が burst するため PgBouncer transaction mode または RDS Proxy / Neon / Supabase のプーラーが実質必須です。関数ごとに new Pool() するとコールドスタートごとに接続が増えます——グローバル変数に 1 プールを置き、max: 1 に抑えるパターンが多いです。

// Lambda: コンテナ再利用時にプールを共有
declare global {
  // eslint-disable-next-line no-var
  var __pgPool: Pool | undefined;
}

export const pool =
  global.__pgPool ??
  new Pool({ max: 1, connectionTimeoutMillis: 3000, ... });

if (process.env.NODE_ENV !== 'production') {
  global.__pgPool = pool;
}

バッチジョブと Web API の分離

重いバッチが Web と同じ pool.max 予算を食うと、ユーザー向け API が枯渇します。

  • 別 DB ロール + 別 PgBouncer プールreserve_pool
  • バッチは 専用ワーカー、接続上限 5 など低く
  • レポート系は 読み取りレプリカ

トラブルシューティングフロー

1

[object Object]

2

[object Object]

3

[object Object]

4

[object Object]

5

[object Object]

緊急時: 問題セッションの切断

本番では慎重に——対象 PID を確認してから実行します。

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND now() - xact_start > interval '5 minutes'
  AND usename = 'app_user';

スーパーユーザー権限が必要です。アプリ側では切断後にプールが壊れた接続を捨て、pool.on('error') でログを残します。

max_connections の引き上げは最後の手段

max_connections を 200 → 500 に上げると、メモリ使用量がほぼ線形増加します。目安として接続 1 本あたり work_mem を使い得るため、安易な引き上げは OOM のリスクがあります。

優先順位:

  1. リーク修正・idle in transaction 排除
  2. PgBouncer 導入 / プールサイズ最適化
  3. 読み取り分散・キャッシュ
  4. それでも足りなければ max_connections 増加 + メモリ増強

Express / Fastify との統合例

Express ミドルウェアでリクエスト ID とタイムアウト

import express from 'express';
import { pool } from './db';

const app = express();

app.use((req, res, next) => {
  req.db = {
    query: <T extends QueryResultRow>(text: string, params?: unknown[]) =>
      pool.query<T>(text, params),
  };
  next();
});

app.get('/health/db', async (_req, res) => {
  try {
    await pool.query('SELECT 1');
    res.json({ ok: true, pool: getPoolStats() });
  } catch (err) {
    res.status(503).json({
      ok: false,
      error: 'database_unavailable',
      pool: getPoolStats(),
    });
  }
});

503 とプール統計を返すことで、ロードバランサのヘルスチェックから問題 Pod を外す判断材料になります。

監視・アラート設計

アラート条件深刻度
PG 接続使用率> 85% が 5 分Warning
PG 接続使用率> 95% が 1 分Critical
idle in transaction> 10 セッションが 60 秒超Warning
PgBouncer cl_waiting> 0 が 30 秒超Critical
アプリ pool.waitingCount> 5 が 1 分超Warning

Prometheus + postgres_exporter の主要メトリクス:

  • pg_stat_database_numbackends
  • pg_settings_max_connections
  • カスタム: PgBouncer SHOW STATS を textfile collector で暴露

ログ相関では、API の エラーハンドリングinstance(リクエスト ID)を付け、DB 側 pg_stat_activity.query と時刻で突き合わせます。

よくある FAQ(実装判断)

Prisma で PgBouncer を使うとき

接続 URL に ?pgbouncer=true(Prisma 5+)を付け、transaction mode 向けの設定を有効にします。Prepared statement キャッシュは無効化が必要な場合があります。

DATABASE_URL="postgresql://user:pass@pgbouncer:6432/appdb?pgbouncer=true&connection_limit=10"

読み取りと書き込みでプールを分ける

export const writePool = new Pool({ connectionString: process.env.DATABASE_URL, max: 15 });
export const readPool = new Pool({ connectionString: process.env.DATABASE_REPLICA_URL, max: 25 });

レプリカ遅延を許容できる GET は readPool へ——書き込みプールの圧力を下げます。

テスト環境での接続数

Jest の maxWorkers × 各テストファイルのプールが接続を食います。テスト用 DB では:

// setup.ts
afterAll(async () => {
  await pool.end();
});

並列テストでは maxWorkers: 1 またはテスト専用スキーマ + 単一プール共有を検討します。

チェックリスト(本番リリース前)

  • max_connections と全インスタンス pool.max の合計を文書化した
  • PgBouncer(またはマネージドプーラー)を経由している
  • pool.connect() すべてに finally { release() } がある
  • idle_in_transaction_session_timeout を設定した
  • application_name でデプロイ単位を識別できる
  • SIGTERM で pool.end() する Graceful shutdown がある
  • 接続使用率・cl_waiting・pool.waitingCount のアラートがある
  • バッチと Web で接続予算を分離した

関連記事

この記事は API・バックエンド テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。

トピック記事
APIキャッシュ設計ガイドAPIキャッシュ設計ガイド|ETag・Cache-Control・304・CDNキャッシュキーの実務
Web APIのべき等性(Idempotency)設計Web APIのべき等性(Idempotency)設計|二重決済を防ぐ実装ガイド
API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較
APIのレート制限(Rate Limiting)実装ガイドAPIのレート制限(Rate Limiting)実装ガイド|アルゴリズム選定と429エラーの適切なハンドリング
APIレート制限と429対応APIレート制限と429対応|X-RateLimit・Retry-Afterを使ったクライアント実装
WebHook設計のベストプラクティスWebHook設計のベストプラクティス|署名・再試行・べき等性で信頼性を担保する
ZodでAPI境界のランタイム検証を設計するZodでAPI境界のランタイム検証を設計する|z.infer・Expressミドルウェア・zod-to-openapi(2026)
OpenAPI 3.1仕様のCIバリデーション完全ガイドOpenAPI 3.1仕様のCIバリデーション完全ガイド|Spectral Lint・Breaking Change検出・GitHub Actions
GraphQL N+1 問題の解決GraphQL N+1 問題の解決|DataLoader バッチング実装と Apollo Server 完全ガイド
Circuit Breaker + Retry パターン実装ガイドCircuit Breaker + Retry パターン実装ガイド|Node.js opossum/cockatiel・指数バックオフ・Jitter
Sagaパターン完全ガイドSagaパターン完全ガイド|Orchestration vs Choreography と補償トランザクション(Node.js)
Event SourcingとCQRSの基礎Event SourcingとCQRSの基礎|Node.js + PostgreSQL 実装ガイド

まとめ

connection pool exhaustedPostgreSQL 接続数上限は、レイヤーを特定すれば対処は体系的です。アプリでは Node.js pgpool.query() 優先release() 保証、インフラでは PgBouncer transaction poolingmax_connections の予算管理、運用では pg_stat_activity による idle in transaction 監視が三本柱です。

接続数は「多いほど速い」リソースではなく、有限の共有資源として設計・監視・テスト(負荷試験で Pod 最大数 × pool.max を再現)することが、ピーク時の連鎖障害を防ぐ最短ルートです。