- データベース
コネクションプールのサイズの決め方|HikariCPの式と実測で小さく始める
結論
DB コネクションプールのサイズは、HikariCP が示す「DB サーバーの CPU コア数 × 2 + 実効ディスク数」を起点に、小さく始めて実測で動かします。手元の 2 コア環境では、CPU を使うクエリの処理量はプール 2〜5 本で頭打ちになり、ロックが絡む更新はプール 5 本が最速で 50 本では処理量が 57% 落ちました。
この記事でわかること
- プール 2・5・10・20・50 本 × 同時 50 リクエストで、処理量・待ち時間・DB の接続数がどう変わるか(PostgreSQL 16・Python・Node で実測)
- max_connections と複数インスタンスの合計の数え方、too many clients など 3 種類のエラー原文の意味
- HikariCP・psycopg_pool・node-postgres・PgBouncer の設定項目の対応と、場面別の判断表
プールのサイズを決めるときに見るものは 3 つです。DB サーバーが同時にさばける数(CPU とディスク)、アプリ側で待たせてよい時間、そして DB 全体の接続数の上限(max_connections)です。
flowchart TD
A[同時リクエスト 50] --> P{プール<br>空きがある?}
P -->|はい| C[接続を借りて SQL 実行]
P -->|いいえ| W[プール内で待つ<br>connectionTimeout まで]
W --> C
C --> D[(PostgreSQL<br>同時実行はプール本数が上限)]
D --> R[返却して次の待ちへ]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| コネクションプール サイズの目安 | DB の CPU コア数 × 2 + 実効ディスク数(HikariCP の式)。4 コアなら 10 前後から | HikariCP の公式の目安 |
| hikaricp maximumpoolsize の既定値 | 10。minimumIdle は既定で maximumPoolSize と同じ(固定サイズ) | 設定項目の対応表 |
| 大きくすれば速くなるか | ならない。CPU を使うクエリは 2 コアで 2〜5 本から頭打ち、ロック競合では遅くなった | 「小さいほど速い」を実測で確かめる |
| 同時リクエストがプールより多いとき | プールの中で並んで待つ。待ち時間 ≒ 同時数 ÷ 処理量(Little の法則) | 待ち行列はどこにできるか |
| max_connections との関係 | 全インスタンスのプール上限 + バッチ + 管理ツール < max_connections − 予約分 | max_connections と複数インスタンスの合計 |
| too many clients already の意味 | DB 全体の上限に達した。プールの合計を見直す | エラー原文と意味 |
| 接続がいつの間にか切れている | DB や LB の idle timeout より maxLifetime を短くする・借りる前に確かめる | 接続の寿命 |
| サーバーレスや多数のインスタンス | PgBouncer・RDS Proxy など外側のプールで DB 側の本数をまとめる | 外側のプール |
確認環境:Ubuntu 24.04.5 LTS(CPU 2 コア・メモリ 8GB の小さな仮想マシン。アプリ側の負荷生成と DB が同じマシンで動いています)に、PostgreSQL 16.15 を初期設定のまま(max_connections = 100・shared_buffers = 128MB)自分のデータディレクトリで 127.0.0.1 に起動し、pgbench 16.15(scale 1)で接続数ごとの処理量を測りました。アプリ側のプールは Python 3.13.18 + psycopg 3.3.7 + psycopg_pool 3.3.3(asyncio)と Node.js 22.23.4 + pg(node-postgres)8.23.1 で、プール 2・5・10・20・50 本に対して同時 50 の自作クライアントを 5 秒間回しました。負荷は 1 回 30 秒以内・同時接続 100 以下に抑えています。HikariCP(README と wiki「About Pool Sizing」、7.1.0 時点)・Spring Boot・PgBouncer(config・features)・PostgreSQL の設定項目は公式ドキュメントで確かめた範囲で、この環境では動かしていません。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月4日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「コネクションプールのサイズの決め方|HikariCPの式と実測で小さく始める」
コネクションプールとは:何を節約し、何を制限するものか
この節では、プールが無いときに毎回払っているコストの大きさと、プールの 2 つの役割が分かります。
コネクションプールは、DB への接続をあらかじめ作って使い回す仕組みで、「接続を作るコストの節約」と「DB に同時に流す数の制限」という 2 つの役割を持ちます。 サイズの話は後者の役割、つまり「DB に何本まで同時に働かせるか」を決めることです。
まず前者のコストを測りました。psycopg で「接続して SELECT 1 を実行して切る」を 200 回と、「1 本をつなぎっぱなしで SELECT 1 を 200 回」を比べた結果です。
新規接続(connect) : 中央値 2.60ms / p95 3.89ms / 最大 6.88ms
新規接続後の SELECT 1 : 中央値 0.41ms / p95 0.56ms / 最大 2.55ms
使い回しの SELECT 1 : 中央値 0.06ms / p95 0.09ms / 最大 0.45ms
接続1回 ≒ SELECT 1 の 42 倍
pgbench でも同じことを確かめました。-C はトランザクションごとに接続し直すオプションです。
$ pgbench -S -c 5 -T 5 -n t245db # 5本をつなぎっぱなし
latency average = 0.181 ms
tps = 27592.599866 (without initial connection time)
$ pgbench -S -c 5 -T 5 -n -C t245db # 毎回つなぎ直す
latency average = 15.538 ms
average connection time = 3.069 ms
tps = 321.788111 (including reconnection times)
PostgreSQL は接続ごとにサーバープロセスを 1 つ fork します。ログ(log_connections = on)には接続のたびに connection received と connection authorized が残り、この記事の実験だけで 1,800 回以上の接続が記録されました。待機中のバックエンドの PSS(共有メモリを按分した実メモリ)は 1 本あたり約 1.35MB でした。接続数が多いこと自体のメモリは大きくありませんが、各接続が work_mem(既定 4MB)をソートやハッシュのたびに使うため、同時に動く本数が増えるほどピークのメモリは読みにくくなります。
PostgreSQL の接続数の見方そのものは、関連記事「PostgreSQL接続数の確認|pg_stat_activityの見方と対処」で扱っています。
HikariCP の公式の目安:connections = core_count × 2 + effective_spindle_count
この節では、いちばん引用される式の正確な中身と、その前提が分かります。
HikariCP の wiki「About Pool Sizing」は、出発点として次の式を示し、「実際の負荷で試して調整する」ことを求めています。
connections = ((core_count * 2) + effective_spindle_count)
| 項目 | 意味 | 注意 |
|---|---|---|
| core_count | DB サーバーの物理コア数 | ハイパースレッドは数えない。アプリサーバーのコア数ではない |
| effective_spindle_count | 実効的なディスク(スピンドル)数 | データが全部キャッシュに乗るなら 0、キャッシュヒット率が下がるほど実ディスク数に近づく |
| SSD | 式の分析対象外 | wiki は「SSD では分析していない」と明記。待ちが短いぶん本数は少なめでよい方向 |
| 例 | 4 コア・ディスク 1 本 → 約 10 | HikariCP の maximumPoolSize の既定値 10 と同じ水準 |
同じページには「小さなプールを、接続待ちのスレッドで飽和させるのが望ましい」という原則(axiom)と、Oracle の実演動画で接続を 2048 本から 96 本に減らしたところ応答時間が約 100ms から約 2ms に縮んだ例が載っています。理由として挙げられているのは CPU(コア数を超えたスレッドはコンテキストスイッチで遅くなる)、ディスク(I/O で止まっている間だけ他のスレッドが進めるので、コア数より少し多くしてよい)、ネットワーク(送受信バッファが詰まると止まる)の 3 つです。
この環境(2 コア・データは全部キャッシュ)に当てはめると 2 × 2 + 0 = 4、ディスクを 1 と見ても 5 です。次の節で、この値が本当に境目になるかを測ります。
「小さいほど速いことがある」を pgbench で確かめる(CPU・ロック・待ちの 3 種類)
この節では、DB に同時に流す本数を 1 本から 50 本まで変えたとき、クエリの種類ごとに処理量と応答時間がどう変わるかが分かります。
CPU を使うクエリは 2〜5 本で処理量が頭打ちになり、ロックが絡む更新は 5 本を超えると処理量が下がり、待つだけのクエリは本数に比例して伸びました。 pgbench の -c(同時クライアント数)は、アプリ側から見れば「プールの本数ぶんだけ常に DB が働いている状態」と同じです。
CPU を使うクエリ(1 回 16ms)
クエリは SELECT count(*) FROM generate_series(1,100000)(単体で 16.3ms)です。
| 同時接続 -c | 処理量 tps | 平均応答 ms |
|---|---|---|
| 1 | 72 | 13.8 |
| 2 | 103 | 19.4 |
| 5 | 116 | 43.0 |
| 10 | 121 | 82.4 |
| 20 | 140 | 143.1 |
| 50 | 136 | 367.2 |
2 コアなので 2 本で CPU はほぼ使い切り、5 本以降は処理量が 116〜140 tps で横ばいです。一方で平均応答時間は本数に比例して伸びました(50 本で 367ms)。本数を増やしても仕事は速くならず、DB の中で順番待ちをする時間が応答時間に乗っただけです。
ロックが絡む更新(pgbench 既定の TPC-B 風)
pgbench の既定スクリプトは 1 トランザクションで 3 つの UPDATE と INSERT を行い、scale 1 では pgbench_branches の 1 行を全員が更新するので、行ロックの競合が起きます。
| 同時接続 -c | 処理量 tps | 平均応答 ms |
|---|---|---|
| 1 | 1,881 | 0.53 |
| 2 | 3,392 | 0.59 |
| 5 | 3,852 | 1.30 |
| 10 | 2,692 | 3.72 |
| 20 | 2,203 | 9.08 |
| 50 | 1,640 | 30.49 |
これが「小さいほど速い」の典型です。同じ行を取り合うトランザクションが増えるほど、ロック待ちとコンテキストスイッチで全員が遅くなります。1 本では CPU が余るので 2〜5 本までは伸び、それを超えると下がりました。
待つだけのクエリ(pg_sleep 10ms)と軽い参照
| 同時接続 -c | pg_sleep(0.01) tps | 主キー参照(-S)tps | -S 平均応答 ms |
|---|---|---|---|
| 1 | 96 | 19,841 | 0.05 |
| 2 | 193 | 21,481 | 0.09 |
| 5 | 476 | 23,532 | 0.21 |
| 10 | 962 | 30,027 | 0.33 |
| 20 | 1,920 | 21,856 | 0.92 |
| 50 | 4,780 | 22,277 | 2.24 |
pg_sleep は CPU を使わずに待つので、本数に比例して処理量が伸びます(応答時間は 10.4ms で一定)。外部 API の応答待ちやディスク I/O 待ちが主なワークロードでは、式の「+ effective_spindle_count」にあたる上乗せが効く、ということです。軽い主キー参照は 10 本で最大(30,027 tps)、それ以上は伸びずに応答時間だけ増えました。
「同時に多く流せば速い」は成り立つか
見分け方:DB の CPU 使用率が高いか、wait_event が IO・Lock か
アプリ側のプールでサイズと同時リクエスト数を変えた実測(psycopg_pool・node-postgres)
この節では、「同時 50 リクエストに対してプールが 2〜50 本」のとき、アプリから見た処理量・接続を借りるまでの待ち時間・応答時間がどうなるかが分かります。
処理量は pgbench と同じ形(CPU は早く頭打ち、待ちは比例)になり、プールを小さくした分の時間は「接続を借りるまでの待ち」に移りました。 2 コアのマシンで負荷生成と DB を同居させているため、絶対値は本番より小さく、Python の値は負荷生成側の CPU も含んでいます。
Python(asyncio + psycopg_pool)・同時 50
プールは min_size = max_size の固定サイズにし、pool.wait() で全部つながってから 5 秒間回しました。wait は pool.connection() に入るまで、応答は SQL が返るまでの時間です。
| プール本数 | CPU クエリ rps | 借りる待ち p50 / p95 ms | 応答 p95 ms | pg_sleep rps |
|---|---|---|---|---|
| 2 | 63 | 895 / 1,528 | 1,563 | 196 |
| 5 | 114 | 395 / 857 | 945 | 475 |
| 10 | 122 | 336 / 451 | 551 | 906 |
| 20 | 132 | 237 / 336 | 536 | 1,677 |
| 50 | 145 | 0.01 / 0.03 | 452 | 2,868 |
Node.js(pg の Pool)・同時 50
pool.connect() で借り、client.release() で返す形です。pool.waitingCount を 50ms ごとに見て最大値を記録しました。「ロック更新」は UPDATE pgbench_branches SET bbalance = bbalance + 1 WHERE bid = 1 です。
| プール max | CPU クエリ rps | CPU 応答 p95 ms | ロック更新 rps | waitingCount 最大 |
|---|---|---|---|---|
| 2 | 131 | 522 | 13,405 | 48 |
| 5 | 136 | 525 | 16,019 | 45 |
| 10 | 139 | 485 | 18,808 | 40 |
| 20 | 156 | 398 | 16,893 | 30 |
| 50 | 156 | 514 | 15,714 | 0 |
1CPU クエリはプール 2 本で 131 rps
Node の軽いクライアントでは 2 本で CPU を使い切り、50 本にしても 156 rps。応答 p95 はほぼ同じ 500ms 前後で、待つ場所がプールから DB に移っただけ。
2ロック更新は 10 本が最速
単文の autocommit なので pgbench の TPC-B より競合が軽く、山は 10 本に寄った。それでも 50 本では 16% 落ちた。
3待ちは消えず、場所が変わる
プール 50 本では waitingCount が 0 になるが応答は縮まない。プールで待つほうが、DB のメモリと CPU を使わず、待ち時間を数えられる。
Python の CPU クエリでプール 2 本が 63 rps と低いのは、asyncio のクライアント自身が同じ 2 コアで CPU を使っているためです。本番のようにアプリと DB が別のマシンなら、Node の表に近い形になります。
待ち行列はどこにできるか:Little の法則で待ち時間を見積もる
この節では、プールの本数と同時リクエスト数から待ち時間を見積もる計算と、「プールで待つ」ことの利点が分かります。
同時にいるリクエスト数 L = 処理量 λ × 滞在時間 W(Little の法則)です。 DB の処理量が頭打ちなら、同時リクエストを増やしても W が伸びるだけで、それはプールの中で待つか DB の中で待つかの違いしかありません。pgbench の CPU クエリ 50 本の行で確かめると、136 tps × 0.367 秒 ≒ 50 で、50 本全部が常に DB の中で並んでいた計算になります。
- 1DB の処理量の上限を測る(pgbench か本番の最大 tps)例:CPU クエリ 136 tps、更新 3,852 tps
- 2その上限に達する最小の本数を探す例:CPU クエリは 5 本、更新は 5 本。ここがプールの候補
- 3想定の同時リクエスト数 ÷ 処理量 = 待ちを含む応答時間例:同時 50 ÷ 136 tps ≒ 0.37 秒。許容できなければ DB を増強するか SQL を速くする
- 4プールの待ち時間切れ(connectionTimeout)を応答時間の目標より短く待ちきれないリクエストは早く失敗させ、全部を遅くしない
プールの外(DB)で待たせると、待っているぶんだけバックエンドプロセスとメモリ・ロックを占有します。プールの中で待たせると、DB は常に「ちょうど飽和する本数」だけで働き、待ち時間は requests_waiting・waitingCount・HikariCP の pending として数えられ、しきい値を超えたら早く失敗させられます。これが HikariCP が「小さなプールを待ちのスレッドで飽和させる」と言う理由です。
タイムアウトの値そのものの決め方は、関連記事「タイムアウト設計の値の決め方|接続・読み取りの違いと多段の計算例」で扱っています。
max_connections と複数インスタンスの合計(PostgreSQL 16)
この節では、DB 側の上限とプールの合計をどう突き合わせるかと、pg_stat_activity で今の内訳を見る方法が分かります。
プールの上限はインスタンスの数だけ掛け算になり、その合計にバッチ・管理ツール・レプリケーションを足したものが max_connections から予約分を引いた数に収まる必要があります。 PostgreSQL 16 の公式ドキュメントでは、max_connections の既定は通常 100、superuser_reserved_connections の既定は 3、16 で加わった reserved_connections(pg_use_reserved_connections ロール向けの予約)の既定は 0 で、いずれも変更には再起動が要ります。スタンバイでは max_connections をプライマリと同じ以上にする必要がある、とも書かれています。
DB 側の上限を確かめる
SHOW max_connections; と SHOW superuser_reserved_connections;。一般ロールが使えるのは 100 − 3 = 97 本。
プールの合計を数える
API 3 台 × maximumPoolSize 10 = 30、ワーカー 2 台 × 5 = 10、バッチ 5、監視・psql・DBeaver などの管理用 5、合計 50。
余裕を残す
オートスケールやデプロイ中は新旧のインスタンスが同時に存在し、一時的に 2 倍になる。合計は上限の 60〜70% までを目安に。
内訳を pg_stat_activity で確かめる
接続文字列に application_name を付けておくと、どのサービスが何本使っているかが一目で分かる。
2 つのプール(application_name を api-1・api-2、各 10 本)を開いた状態で集計した結果です。
SELECT application_name, usename, state, count(*)
FROM pg_stat_activity WHERE backend_type = 'client backend'
GROUP BY 1,2,3 ORDER BY 1,2,3;
application_name | usename | state | count
------------------+----------+---------------------+-------
api-1 | t245app | idle | 9
api-1 | t245app | idle in transaction | 1
api-2 | t245app | idle | 10
psql | postgres | active | 1
SELECT current_setting('max_connections') AS max_connections,
current_setting('superuser_reserved_connections') AS reserved,
(SELECT count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend') AS now_used;
max_connections | reserved | now_used
-----------------+----------+----------
100 | 3 | 21
固定サイズのプールは、使っていなくても本数ぶんの idle を占めます。idle in transaction が長く残っているなら、トランザクションを開いたまま接続を返していないか、autocommit を切ったまま SELECT だけしている箇所があります。接続数の見積もり全体は、関連記事「キャパシティ見積もりの計算方法|同時接続数・スループット・ディスク容量」で扱っています。
too many clients already など:エラー原文と意味(DB 側 3 種・プール側 3 種)
この節では、接続数まわりで出るエラーの原文と、それぞれが「どこの上限」に当たったのかが分かります。
DB 側の上限は 3 段階(ロールの CONNECTION LIMIT → 一般ロールの枠 → 全体)で、どの文言が出たかで見直す場所が決まります。 この環境で実際に接続を増やして出した原文です。
CREATE ROLE t245app LOGIN CONNECTION LIMIT 5;
t245app 接続 1 … 5
6本目 ERR: FATAL: too many connections for role "t245app"
ALTER ROLE t245app CONNECTION LIMIT -1; -- 制限を外して増やし続ける
一般ロール: 97 本目まで成功、98 本目で ERR:
FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute
superuser: さらに 3 本成功、合計 101 本目で ERR:
FATAL: sorry, too many clients already
pg_stat_activity: (100,)
| 原文 | 当たった上限 | 見直す場所 |
|---|---|---|
too many connections for role "…" |
そのロールの CONNECTION LIMIT | ロールの制限値か、そのロールを使うプールの合計 |
remaining connection slots are reserved for roles with the SUPERUSER attribute |
max_connections − 予約 3(一般ロールの枠) | 全インスタンスのプール合計。管理者はまだ入れるので pg_stat_activity で犯人を探せる |
sorry, too many clients already |
max_connections 全体 | 同上。管理者も入れないので、まず既存の接続を切る |
psycopg_pool:couldn't get a connection after 2.00 sec(PoolTimeout) |
プールの timeout | 借りっぱなし(リーク)・遅い SQL・本数不足の順に疑う |
node-postgres:timeout exceeded when trying to connect |
Pool の connectionTimeoutMillis | 同上。既定は 0(無期限に待つ)なので値を決めておく |
HikariCP:Connection is not available, request timed out after 30000ms. |
connectionTimeout(既定 30 秒) | 同上。HikariCP のソースコードにある文言(この環境では動かしていない) |
DB 側の 3 つはいずれも SQLSTATE 53300(too_many_connections)で、node-postgres では err.code、psycopg では e.sqlstate で判定できます。Node で CONNECTION LIMIT 2 のロールに 3 本同時に投げた結果は次のとおりです。
DB上限 Error: too many connections for role "t245lim" / code: 53300
too many clients の応急処置と直し方の全体は、関連記事「PostgreSQL too many clientsの直し方|接続数と接続プール」で扱っています。この記事は、そのエラーを出さないための「本数の決め方」に絞っています。
プールの待ち時間切れ・リーク・プール詰まり(pool-locking)の再現
この節では、プールが足りなくなる 3 つの典型(返し忘れ、1 スレッドが複数本を要する、待ちきれない)を小さなコードで再現し、見分け方が分かります。
プールが空になる原因の多くはサイズではなく、「返していない」か「1 つの処理が 2 本以上を同時に握る」ことです。 まず max_size 2 のプールで 2 本を借りたまま返さず、3 本目を要求しました。
with ConnectionPool(DSN, min_size=2, max_size=2, timeout=2, open=True) as pool:
pool.wait()
held = [pool.getconn(), pool.getconn()] # 2本とも返さない(リーク相当)
try:
with pool.connection():
pass
except PoolTimeout as e:
print("PoolTimeout:", e)
print(pool.get_stats())
2.00s 後 PoolTimeout: couldn't get a connection after 2.00 sec
get_stats: {'requests_queued': 1, 'requests_errors': 1, 'requests_wait_ms': 2000,
'pool_size': 2, 'pool_available': 0, 'requests_waiting': 1}
pool_available が 0 のまま requests_waiting が増え続け、SQL は速いのに待ちが長いなら、返し忘れです。HikariCP では leakDetectionThreshold(既定 0 = 無効、有効にできる最小値 2000ms)を設定すると、借りてからその時間を過ぎた接続についてスタックトレース付きの警告を出す、と README にあります。開発環境で有効にしておくと、返し忘れの場所が特定できます。
1 スレッドが 2 本以上を握る:pool size = Tn × (Cm − 1) + 1
wiki「About Pool Sizing」の「Pool-locking」の節は、1 つの処理が同時に複数の接続を必要とする設計ではデッドロックが起きるとし、それを避ける最小のプールサイズを Tn × (Cm − 1) + 1(Tn = 最大スレッド数、Cm = 1 スレッドが同時に握る最大本数)としています。4 スレッドが各 2 本を要する場合、4 × 1 + 1 = 5 本です。4 本と 5 本で試しました。
def worker(i):
try:
with pool.connection() as a: # 1本目
time.sleep(0.2)
with pool.connection() as b: # 2本目(ここで全員が待つ)
res.append("ok")
except PoolTimeout:
res.append("timeout")
プール4本: ['timeout', 'timeout', 'ok', 'ok']
プール5本(Tn×(Cm−1)+1 = 4×1+1): ['ok', 'ok', 'ok', 'ok']
4 本では 4 スレッド全員が 1 本ずつ握って 2 本目を待ち、3 秒のタイムアウトで 2 つが失敗して初めて残りが進みました。5 本では全員が通ります。wiki はこれを「最適値ではなくデッドロックを避ける最小値」と呼び、まずアプリ側で「1 処理 1 接続」にする(JTA などトランザクション内で同じ接続を再利用する)ことを勧めています。ネストしたトランザクションや、トランザクション中に別のリポジトリが新しい接続を取りに行く ORM の設定は、この形になりやすい箇所です。
スレッドプールとの関係では、「アプリのワーカースレッド数 > プール本数」が正常な状態で、逆だと接続が余ります。スレッド数の決め方は、関連記事「Java ExecutorServiceの使い方|スレッドプールと仮想スレッド」で扱っています。
接続の寿命:maxLifetime と DB・LB 側の idle timeout の関係
この節では、プールに入れっぱなしの接続が外側(DB・ロードバランサー・ファイアウォール)から切られるとどうなるか、どの設定で防ぐかが分かります。
プールは接続を長く持つので、DB や経路の機器の「無通信で切る」時間より先に、プール側で捨てて作り直す必要があります。 PostgreSQL の idle_session_timeout をロールに 2 秒で設定し、1 本だけのプールで 3 秒空けて使うとどうなるかを見ました。
ALTER ROLE t245app SET idle_session_timeout = '2s';
1回目: (1,)
discarding closed connection: <psycopg.Connection [BAD] at 0x7fe751343460>
2回目 ERR: IdleSessionTimeout - terminating connection due to idle-session timeout
3回目(プールが作り直した後): (1,)
-- check=ConnectionPool.check_connection を付けた場合
1回目: (1,)
2回目: (1,)
-- PostgreSQL のログ
FATAL: terminating connection due to idle-session timeout
何もしないと、切られた後の最初のリクエストがエラーになり、プールがその接続を捨てて次から回復します。check(借りる前の確認)を付けると、死んだ接続を渡さずに作り直すのでエラーは出ませんが、毎回の往復が 1 つ増えます。HikariCP の README は次の値を示しています(7.1.0 時点)。
| HikariCP の項目 | 既定 | 公式の説明の要点 |
|---|---|---|
| maxLifetime | 1,800,000ms(30 分) | DB やインフラが課す接続の時間制限より「数秒短く」する。使用中の接続は返却後に引退 |
| keepaliveTime | 120,000ms(2 分) | 待機中の接続に定期的に ping し、DB やネットワークのタイムアウトを防ぐ。maxLifetime より短く |
| idleTimeout | 600,000ms(10 分) | minimumIdle < maximumPoolSize のときだけ有効。待機中の接続を減らす |
| connectionTestQuery | なし | JDBC4 の isValid() が無い旧式ドライバー向け。通常は設定しない |
| validationTimeout | 5,000ms | 生存確認の最大時間。connectionTimeout より短く |
- 1経路で最も短い「無通信で切る」時間を調べるPostgreSQL の idle_session_timeout、クラウドの LB・NAT のアイドルタイムアウト、ファイアウォール
- 2プールの寿命をそれより短くHikariCP maxLifetime・psycopg_pool max_lifetime(既定 3,600 秒)・pg maxLifetimeSeconds(既定 0 = 無効)
- 3keepalive か借りる前の確認を 1 つ選ぶHikariCP keepaliveTime、psycopg_pool check、pg は TCP keepalive か短い idleTimeoutMillis(既定 10 秒)
- 4全部が同時に入れ替わらないようにするpsycopg_pool は max_lifetime を最大 5% ランダムに縮める。HikariCP も maxLifetime に接続ごとの小さな負の変動を入れると README にある
HikariCP・psycopg_pool・node-postgres の設定項目の対応表
この節では、3 つのプールで「同じ意味の設定」がどの名前かと、その既定値が分かります。
名前は違っても、決める値は「上限」「下限(常時保持)」「借りる待ち時間」「寿命」「待機中の寿命」「リーク検出」の 6 つです。 既定値はいずれも公式ドキュメント(2026年11月4日時点)によります。
| 決めること | HikariCP(Java) | psycopg_pool(Python) | pg Pool(Node) |
|---|---|---|---|
| 上限 | maximumPoolSize(10) | max_size(None = min_size と同じ) | max(10) |
| 常時保持 | minimumIdle(= maximumPoolSize) | min_size(4) | min(0。先回りして作らない) |
| 借りる待ち時間 | connectionTimeout(30,000ms) | timeout(30 秒) | connectionTimeoutMillis(0 = 無期限) |
| 寿命 | maxLifetime(30 分) | max_lifetime(3,600 秒) | maxLifetimeSeconds(0 = 無効) |
| 待機中の寿命 | idleTimeout(10 分) | max_idle(600 秒・min_size 超過分のみ) | idleTimeoutMillis(10,000ms) |
| リーク検出 | leakDetectionThreshold(0 = 無効) | なし(get_stats で requests_waiting を見る) | なし(waitingCount を見る) |
| 待ち行列の上限 | なし(スレッド数が上限) | max_waiting(0 = 無制限、超えると TooManyRequests) | なし |
HikariCP の README は、minimumIdle を設定せず 固定サイズのプールとして動かすことを推奨しています。psycopg_pool も max_size を省くと固定サイズです。Node の pg は min の既定が 0 で、使われるまで接続を作りません。Spring Boot では spring.datasource.hikari.* の下に同じ名前(ケバブケース)で書きます。
# Spring Boot(公式ドキュメントの形式。値は例)
spring:
datasource:
hikari:
maximum-pool-size: 10
connection-timeout: 3000
max-lifetime: 1500000
keepalive-time: 120000
leak-detection-threshold: 2000 # 開発環境で有効にする例
# psycopg_pool(この記事の実測で使った形)
from psycopg_pool import ConnectionPool
pool = ConnectionPool(
"host=127.0.0.1 dbname=app user=app application_name=api-1",
min_size=5, max_size=5, # 固定サイズ
timeout=3, # 借りる待ち時間(秒)
max_lifetime=1500, # DB・LB の idle timeout より短く
check=ConnectionPool.check_connection,
open=False,
)
pool.open(); pool.wait()
// node-postgres
const { Pool } = require('pg');
const pool = new Pool({
max: 5,
connectionTimeoutMillis: 3000, // 既定 0(無期限)のままにしない
idleTimeoutMillis: 10000,
maxLifetimeSeconds: 1500,
application_name: 'api-1',
});
pool.on('error', (err) => console.error('idle client error', err)); // 付けないとプロセスが落ちる
外側のプール:PgBouncer・RDS Proxy はいつ使うか(公式の範囲)
この節では、アプリ内のプールだけでは足りなくなる場面と、PgBouncer のモードごとの制約が分かります。
インスタンスが多い・サーバーレスで接続が短命・言語ごとにプールがばらばら、という状況では、DB の手前に 1 つのプールを置いて DB 側の本数をまとめます。 PgBouncer の公式ドキュメント(config・features)によると、設定の中心は次のとおりです。この環境では動かしていないので、記述は公式の範囲です。
| 項目 | 既定 | 意味 |
|---|---|---|
| pool_mode | session | session:クライアント切断まで同じサーバー接続。transaction:トランザクション終了で返す。statement:クエリごと(複数文のトランザクション不可) |
| default_pool_size | 20 | ユーザー/DB の組ごとの DB 側接続の上限。ここが実質の「DB に流す本数」 |
| max_client_conn | 100 | PgBouncer が受け付けるクライアント接続の上限(OS の fd 上限も要確認) |
| max_db_connections | 0(無制限) | DB ごとの DB 側接続の上限(全ユーザー合計) |
| reserve_pool_size | 0 | 待ちが reserve_pool_timeout(5 秒)を超えたときだけ使う予備 |
| server_lifetime / server_idle_timeout | 3,600 秒 / 600 秒 | DB 側接続の寿命と待機中の寿命(アプリ内プールの maxLifetime に相当) |
| query_wait_timeout | 120 秒 | サーバー接続が割り当てられないまま待てる時間。超えると切断 |
transaction モードはクライアント 1,000 本を DB 側 20 本にまとめられる一方で、features ページの表では SET/RESET・LISTEN・WITH HOLD CURSOR・SQL の PREPARE/DEALLOCATE・PRESERVE/DELETE ROWS の一時テーブル・LOAD・セッションレベルの勧告ロック が「使えない」とされています。プロトコルレベルのプリペアドステートメントは max_prepared_statements で追跡されます。アプリ側のプールと二重に持つ場合、アプリ側は「そのインスタンスが同時に使う本数」、PgBouncer 側は「DB が同時にさばく本数」と役割を分け、後者を HikariCP の式で決めます。AWS の RDS Proxy も同じ位置に置くプールで、Lambda のように接続が短命な構成で使われます。
アプリ内のプールだけでよい
インスタンス数が少なく、合計が max_connections の 6〜7 割に収まる
SET・LISTEN・一時テーブル・勧告ロックを使う
プリペアドステートメントに強く依存している
外側のプールを足す
インスタンスが数十以上、またはサーバーレスで接続が短命
複数言語・複数サービスが 1 つの DB を共有する
DB 側の本数を 1 か所で上限管理したい
設定値の判断表(ワークロード別・環境別)
この節では、自分の条件に当てはめて最初の値を決め、どの指標を見て動かすかが分かります。
最初の値は式で、動かす判断は「プールの待ち」と「DB の CPU・wait_event」の 2 つで行います。
| 条件 | 最初の値 | 増やす・減らすサイン |
|---|---|---|
| 短い SQL 中心の Web API(DB 4 コア・SSD・キャッシュに乗る) | 8〜10(4 × 2 + 0〜1) | 待ちが増えても DB の CPU が 70% 未満なら少し増やす。CPU が張り付いているなら増やさず SQL を速くする |
| ロック競合のある更新(同じ行・同じ口座・在庫) | コア数前後(2 コアで 5 が最速だった) | wait_event が Lock なら本数を減らすほうが速いことがある |
| I/O・外部 API の待ちが長い処理 | 式より多め(待ちの割合に応じて) | DB の CPU が余っていて pg_sleep 型なら比例して伸びる。ただし max_connections の合計内で |
| 長いバッチとオンラインが同居 | 別々のプールに分ける(バッチは 1〜2 本) | wiki も「長短の混在は 2 つのプール」を勧めている |
| インスタンスが多い・サーバーレス | 各 2〜5 + 外側のプールで DB 側を式の値に | 合計が予約分を引いた max_connections の 6〜7 割を超えたら外側のプールへ |
| 2 コアの検証環境(この記事) | 4〜5 | CPU クエリは 2 本で飽和、更新は 5 本が最速、50 本で −57% |
式で起点を出す
DB のコア数 × 2 + 実効ディスク数。アプリのコア数やスレッド数ではない。
4 コアなら 8〜10
合計を上限と突き合わせる
インスタンス数 × 上限 + バッチ + 管理用 < (max_connections − 予約) × 0.7。
デプロイ中の 2 倍を忘れない
待ち時間切れを短く
connectionTimeout は応答時間の目標より短く(数秒)。無期限の既定(pg の 0)は変える。
早く失敗させる
2 つの指標で動かす
プールの待ち(pending・requests_waiting・waitingCount)と DB の CPU・wait_event。待ちがあって CPU が余るなら増やす、CPU が張り付くなら増やさない。
1 回に 1 つ変えて測る
よくあるつまずき:症状→原因→直し方
この節では、プールのサイズに関係して起きやすい症状と、その切り分けが分かります。
| 症状 | 原因 | 直し方 |
|---|---|---|
| 起動直後は速く、数時間で待ちが増えてタイムアウト | 返し忘れ(例外時に close されない・async で release が呼ばれない) | try-with-resources・with・finally で返す。HikariCP は leakDetectionThreshold を開発環境で有効に |
| プールを 10 → 50 にしたら全体が遅くなった | DB の CPU かロックで飽和。待つ場所が DB に移り、コンテキストスイッチが増えた | 式の値に戻し、SQL とインデックスを直す。更新の競合は本数を減らすほうが速い |
| デプロイのたびに too many clients | 新旧インスタンスが同時に存在し、minimumIdle ぶんの接続が 2 倍になる | 合計を上限の 6〜7 割に。固定サイズをやめて minimumIdle を下げるか、外側のプールを置く |
| 深夜など暇な時間の最初のリクエストだけ失敗する | DB・LB・NAT の idle timeout で接続が切られ、プールが気づいていない | maxLifetime を経路で最短のタイムアウトより短く。keepalive か借りる前の確認を入れる |
| SQL は 1ms なのに応答が 300ms | プールの待ち。同時リクエストが本数を大きく超えている | 待ち時間を計測して可視化。DB の CPU が余るなら本数を増やし、余らないなら上流で同時数を絞る |
| 特定の処理だけ時々固まり、タイムアウトで一斉に動き出す | 1 処理が 2 本以上を同時に握る pool-locking | 1 処理 1 接続に直す。無理なら Tn × (Cm − 1) + 1 以上にする |
| pg_stat_activity に idle in transaction が溜まる | トランザクションを開いたまま返している・autocommit 無しで SELECT だけ | 返す前に commit/rollback。psycopg_pool の connection() は正常終了で commit・例外で rollback してから返すが、設計として開きっぱなしにしない |
| PgBouncer の transaction モードにしたら SET や LISTEN が効かない | セッション単位の機能はトランザクションモードで使えない(公式の features 表) | session モードにするか、その機能を使わない設計に変える |
よくある質問
Q. 結局、最初はいくつにすればよいですか。
DB サーバーのコア数 × 2 + 実効ディスク数(SSD でキャッシュに乗るなら 0〜1)です。4 コアなら 8〜10 で、HikariCP の既定 10 はこの水準です。そこから、プールの待ちと DB の CPU を見て 1 回に 1 つずつ動かします。
Q. アプリサーバーのコア数やスレッド数で決めるのではないのですか。
違います。式のコア数は DB サーバーのものです。アプリ側のスレッド数はプール本数より多くてよく、その差が「プールで待つ」状態になります。アプリ側のスレッド数に合わせて本数を増やすと、待つ場所が DB に移るだけです。
Q. 同時ユーザーが 1,000 人いてもプール 10 本でよいのですか。
多くの場合は足ります。1 リクエストが DB を使う時間は数ミリ秒で、1,000 人が同時に SQL を実行している瞬間はほとんどありません。足りるかどうかは「同時に DB を使う数 ÷ 処理量」の待ち時間で判断します。この環境では同時 50 に対してプール 5 本で、CPU クエリの処理量は 50 本のときの 8 割に達していました。
Q. minimumIdle を小さくしてメモリを節約すべきですか。
HikariCP の README は minimumIdle を設定せず固定サイズにすることを推奨しています。接続を増減させるコスト(接続 1 回 2.6ms+認証)と、急な負荷のときに本数が足りない時間を避けるためです。待機中のバックエンド 1 本は約 1.35MB(この環境)で、数十本なら節約の効果は小さいです。インスタンスが多くて合計が上限に迫るときだけ下げます。
Q. connectionTimeout(借りる待ち時間)はどれくらいにしますか。
HikariCP の既定 30 秒は長すぎることが多く、API なら応答時間の目標より短い数秒にします。待ちきれないリクエストを早く失敗させることで、全リクエストが一様に遅くなるのを防ぎます。node-postgres の既定 0(無期限)は必ず値を入れます。
Q. 読み取りレプリカがあるときはどう数えますか。
プライマリとレプリカは別の DB サーバーなので、それぞれのコア数で式を立て、それぞれの max_connections と突き合わせます。PostgreSQL の公式ドキュメントでは、スタンバイの max_connections はプライマリと同じ以上にする必要があります。
Q. プールの待ちを監視するには何を見ますか。
HikariCP は Micrometer 経由の hikaricp.connections.pending・active・timeout、psycopg_pool は get_stats() の requests_waiting・requests_wait_ms・requests_errors、node-postgres は pool.waitingCount・totalCount・idleCount です。DB 側は pg_stat_activity の state と wait_event_type を一緒に見ます。
Q. max_connections を 1,000 に上げれば解決しませんか。
エラーは消えますが、DB の中で待つ本数が増えるだけで速くはなりません。この環境では更新処理が 5 本から 50 本で処理量 −57% でした。上げるのは「同時に働かせたい本数の合計」が本当に多い場合で、その前に外側のプールで本数をまとめるほうが効きます。
Q. Lambda やサーバーレスではプールはどうしますか。
実行環境ごとに 1〜2 本の小さなプール(または接続 1 本)にし、DB の手前に RDS Proxy・PgBouncer などの外側のプールを置いて DB 側の本数を式の値にまとめます。実行環境の数は予測できないので、アプリ内プールの合計で上限を守るのは難しいためです。
Q. 2 コアの環境の結果を本番に当てはめてよいですか。
絶対値は当てはまりません。形(CPU は早く飽和、ロック競合は増やすと遅い、待ちは比例)と、式が境目と一致したことを持ち帰り、本番相当の DB で pgbench か実負荷で同じ曲線を描いて決めます。
まとめ
- プールのサイズは DB サーバーの「コア数 × 2 + 実効ディスク数」から始める。4 コアなら 8〜10、HikariCP の既定 10 はこの水準。
- 大きくしても速くならない。2 コアの実測では CPU クエリは 2〜5 本で頭打ち、ロック競合のある更新は 5 本が最速で 50 本では −57%、待つだけの処理だけが比例して伸びた。
- 待ちは消えず場所が変わる。プールの中で待たせるほうが DB の資源を使わず、待ち時間を数えて早く失敗させられる。
- 合計で考える。インスタンス数 × 上限 + バッチ + 管理用 を max_connections − 予約 3 の 6〜7 割に収め、超えるなら PgBouncer などの外側のプールへ。
- サイズより先に、返し忘れ・1 処理で複数本・DB や LB の idle timeout を疑う。maxLifetime は経路で最短のタイムアウトより短くする。
参考・出典
確認日はいずれも 2026年11月4日です。
- HikariCP wiki「About Pool Sizing」:https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
- HikariCP README(Configuration (knobs, baby!)・7.1.0):https://github.com/brettwooldridge/HikariCP
- PostgreSQL 16「Connections and Authentication」(max_connections・superuser_reserved_connections・reserved_connections):https://www.postgresql.org/docs/16/runtime-config-connection.html
- PostgreSQL 16「Appendix A. PostgreSQL Error Codes」(53300 too_many_connections):https://www.postgresql.org/docs/16/errcodes-appendix.html
- PostgreSQL 16「pgbench」:https://www.postgresql.org/docs/16/pgbench.html
- psycopg 3「Connection pools」「psycopg_pool – Connection pool implementations」:https://www.psycopg.org/psycopg3/docs/api/pool.html
- node-postgres「pg.Pool」:https://node-postgres.com/apis/pool
- PgBouncer「Config」「Features」:https://www.pgbouncer.org/config.html / https://www.pgbouncer.org/features.html
- Spring Boot Reference「Data」(spring.datasource.hikari.*):https://docs.spring.io/spring-boot/reference/data/sql.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「コネクションプールのサイズの決め方|HikariCPの式と実測で小さく始める」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。