PostgreSQL接続プール枯渇の対処法|too many connections・PgBouncer・Node.js pg
- 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_connections | 100 | 同時接続のハード上限 |
superuser_reserved_connections | 3 | スーパーユーザー専用の予約スロット |
一般ロールが使える接続数は実質 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つの典型原因
[object Object]
[object Object]
[object Object]
[object Object]
[object Object]
原因1: 接続リーク(最も多い)
pool.connect() で借りた client を release() しないと、そのスロットはプロセスが死ぬまでプールから消えません。リクエスト 1 万本処理すれば、理論上 1 万本の接続がプール上限を食い尽くします(実際は max に達した時点で停止)。
リークは例外パスで顕在化します。
// ❌ 危険: クエリ失敗時に release されない
const client = await pool.connect();
const res = await client.query('SELECT ...'); // ここで throw
client.release(); // 到達しない
原因2: 水平スケール時の積み上げ
オートスケールで Pod 数が増えると、各 Pod が独立したプールを持ちます。
| 構成 | pool.max | インスタンス | 合計要求 |
|---|---|---|---|
| 開発 | 10 | 1 | 10 |
| ステージング | 20 | 2 | 40 |
| 本番(平常) | 20 | 4 | 80 |
| 本番(スケールアウト) | 20 | 20 | 400 |
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-postgres(pg)の 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 バウンドな短い API | max = 10〜20 |
| 重い JOIN / レポート | max = 5〜10、別読み取り専用プール |
| PgBouncer transaction mode | max はやや大きくても 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 を問わず必須
}
}
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 では terminationGracePeriodSeconds を query_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 POOLS の cl_active / cl_waiting / sv_active を監視します。cl_waiting が継続的に > 0 なら、プールサイズ不足または DB 側の遅延クエリが原因です。
| カラム | 意味 |
|---|---|
cl_active | クライアントがクエリ実行中 |
cl_waiting | プールの空き待ち(要警戒) |
sv_active | PostgreSQL 側で実行中 |
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 が数十秒続く → ページング
| メトリクス | 正常時 | 異常時 |
|---|---|---|
waitingCount | 0 が大半 | スパイク時のみ短時間 |
totalCount | max 未満 | 常に 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に相当。QueryRunnerはrelease()必須 - マイグレーション: デプロイ時に全インスタンスが同時 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 など低く
- レポート系は 読み取りレプリカへ
トラブルシューティングフロー
[object Object]
[object Object]
[object Object]
[object Object]
[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 のリスクがあります。
優先順位:
- リーク修正・idle in transaction 排除
- PgBouncer 導入 / プールサイズ最適化
- 読み取り分散・キャッシュ
- それでも足りなければ
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_numbackendspg_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 exhausted と PostgreSQL 接続数上限は、レイヤーを特定すれば対処は体系的です。アプリでは Node.js pg の pool.query() 優先と release() 保証、インフラでは PgBouncer transaction pooling と max_connections の予算管理、運用では pg_stat_activity による idle in transaction 監視が三本柱です。
接続数は「多いほど速い」リソースではなく、有限の共有資源として設計・監視・テスト(負荷試験で Pod 最大数 × pool.max を再現)することが、ピーク時の連鎖障害を防ぐ最短ルートです。