目次
アクセスが増えたある日、アプリのログに FATAL: sorry, too many clients already や Too many connections が並び、サーバーのCPUもメモリも余っているのにDBに繋がらなくなる。Lambdaやサーバーレス関数からDBを使い始めたら、同時実行が増えた瞬間に同じエラーが出た。こうした「接続数の壁」は、SQLの速さとは別の次元で起きる問題です。
この記事では、なぜDB接続は「作るのが高く、保持するのも高い」のかを仕組みから説明し、アプリ内のコネクションプール(Prisma・SQLAlchemy・HikariCP)と外部プーラー(PgBouncer・ProxySQL)の違い、PgBouncerのsession/transactionモードとその制約、サーバーレス環境で接続が枯渇する構造的な理由、そして設定値の目安までを扱います。PostgreSQLの基本操作はPostgreSQL入門を前提にしています。
DB接続はなぜ高コストなのか
接続確立には往復と認証とプロセス生成が伴う
アプリがDBに接続するとき、内部ではTCPの3ウェイハンドシェイク、TLSのネゴシエーション(有効なら)、認証(パスワードのハッシュ交換)、セッション初期化が順に行われます。ネットワーク越しなら数ミリ秒〜数十ミリ秒かかり、1回のクエリが1ミリ秒で終わる場合、接続確立のほうがはるかに重い処理になります。
さらにPostgreSQLは接続1本ごとに専用のバックエンドプロセスをforkします。プロセス生成のコストに加え、各プロセスは work_mem などのメモリを個別に消費するため、接続が増えるほどDBサーバーのメモリを圧迫します。MySQLは接続ごとにスレッドを使うため生成コストはやや軽いものの、スレッドごとのバッファが積み上がる点は同じです。「リクエストのたびに接続して切断する」実装が遅く、しかもDBを不安定にする理由がここにあります。
max_connectionsは「上げれば解決」ではない
PostgreSQLの max_connections はデフォルト100です。これを1000に上げること自体は可能ですが、1000本の接続が同時にクエリを実行すればCPUコア数をはるかに超える並列度になり、コンテキストスイッチとロック競合で全体の処理速度がむしろ落ちます。DBが効率よく処理できる同時クエリ数はコア数の数倍程度が目安で、それ以上の接続は「待たせる」のが正しい設計です。この「待たせる」役割を担うのがコネクションプールです。マネージドDBではインスタンスサイズごとに上限が決まっており、RDSではメモリ量から自動計算された値が使われます。
アプリ内プールと外部プーラーの違い
アプリ内プール: プロセスごとに接続を使い回す
ほとんどのORMやDBドライバは、確立した接続をプロセス内で保持して再利用するアプリ内プールを持っています。接続を毎回作らずに済むため、まずこれを正しく設定するのが第一歩です。
# SQLAlchemy(Python): プール上限5 + 一時的な超過10、待ち時間30秒
engine = create_engine(
"postgresql+psycopg://user:pass@db:5432/app",
pool_size=5, max_overflow=10, pool_timeout=30,
pool_pre_ping=True, # 死んだ接続を使う前に検査する
pool_recycle=1800, # 30分で接続を作り直す(NATやLBのタイムアウト対策)
)
# Prisma(Node.js): 接続文字列で指定。デフォルトは CPU数×2+1
# DATABASE_URL="postgresql://user:pass@db:5432/app?connection_limit=5&pool_timeout=10"
# HikariCP(Java): 公式が推奨する小さめの値から始める
# spring.datasource.hikari.maximum-pool-size=10
# spring.datasource.hikari.connection-timeout=30000
アプリ内プールの限界は「プロセスの数だけ独立して存在する」ことです。Gunicornでワーカー4つ、サーバー3台なら12プロセス。それぞれがプール上限10を持てば、DBには最大120本の接続が張られます。ここにバッチやワーカーや管理画面が加わると、各アプリの設定値は小さくても合計がmax_connectionsを超えます。Prismaなど各ORMの設定方法は個別記事も参照してください。
外部プーラー: DBの手前で接続を集約する
PgBouncer(PostgreSQL用)やProxySQL(MySQL用)は、アプリとDBの間に立つ独立したプロセスです。アプリからは数千本の接続を受け付けつつ、DBへは数十本だけを張り、アプリ側の接続にDB側の接続を貸し出す形で多重化します。アプリ側はPgBouncerのポート(デフォルト6432)に繋ぐだけで、コードの変更は接続先の変更のみです。
| アプリ内プール | 外部プーラー(PgBouncer等) | |
|---|---|---|
| 設置場所 | 各アプリプロセスの中 | アプリとDBの間の独立プロセス |
| 集約の範囲 | 1プロセス内 | 全アプリ・全サーバー横断 |
| 追加の運用対象 | なし | プーラー自体の監視・冗長化が必要 |
| 向いている状況 | プロセス数が少なく合計が収まる | プロセス数が多い、サーバーレス、複数アプリが同一DBを使う |
PgBouncerのモードと制約
session・transaction・statementの違い
PgBouncerの挙動は pool_mode で決まり、「いつDB接続をアプリに貸し、いつ返してもらうか」が異なります。
- session(デフォルト): アプリが接続している間ずっと1本貸し出す。多重化の効果はほぼなく、接続確立コストの削減だけ。制約はないが、集約もされない。
- transaction: トランザクション単位で貸し出し、COMMIT/ROLLBACKで返してもらう。実務でPgBouncerを使う理由のほとんどはこのモードで、数百のアプリ接続を数十のDB接続に集約できる。
- statement: 1文ごとに返す。複数文のトランザクションが使えないため、特殊用途のみ。
transactionモードでは、「トランザクションをまたいで状態を持つ機能」が使えません。SET によるセッション変数、LISTEN/NOTIFY、名前付きプリペアドステートメント(PgBouncer 1.21以降はプロトコルレベルの対応が追加されましたが、設定が必要)、アドバイザリロックの一部、一時テーブルなどです。ORMが内部でプリペアドステートメントを使う場合、?pgbouncer=true(Prisma)や prepare_threshold=0(psycopg)のような設定が必要になります。
# /etc/pgbouncer/pgbouncer.ini(抜粋)
[databases]
app = host=127.0.0.1 port=5432 dbname=app
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000 # アプリ側から受け付ける上限
default_pool_size = 20 # DB側に張る接続数(ユーザー・DBの組み合わせごと)
reserve_pool_size = 5 # 急増時の一時的な追加分
server_idle_timeout = 600
# 監視用: psql -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
SHOW POOLS の cl_waiting(DB接続待ちのクライアント数)が常に0でなければ default_pool_size が足りず、sv_idle が多ければ余っています。この2つを見ながら調整するのが基本です。
サーバーレス環境で接続が枯渇する理由
「プロセスが増減する」と「プールが増減する」は同じ意味
AWS LambdaやCloud Run、Vercel Functionsでは、同時リクエスト数に応じてインスタンスが自動で増えます。各インスタンスはアプリ内プールを持つため、100並列になれば100個のプールが生まれ、それぞれが数本ずつ接続を張ります。しかもインスタンスは処理後しばらく生きているので、接続は保持されたままです。結果として、ピーク時にはmax_connectionsを一瞬で使い切ります。
対策は「各インスタンスのプールを1本に絞る」だけでは不十分で、外部プーラーで集約するのが定石です。RDS Proxy、Supabaseに組み込まれたSupavisor、NeonやPlanetScaleのHTTP接続、Prisma Accelerateなどはすべてこの問題に対する製品側の答えであり、自前でPgBouncerを立てる代わりに使えます。サーバーレスの特性はサーバーレスアーキテクチャとは?で解説しています。
トラブル事例:夜間バッチの時間だけWebが「too many clients」になる
症状
毎日2時前後にWebアプリで数分間DBエラーが発生し、PostgreSQLのログに FATAL: sorry, too many clients already が記録されていました。Webアプリのプール設定は各ワーカー5本、合計40本で、max_connections=100 には余裕があるはずでした。
原因
SELECT count(*), usename, state FROM pg_stat_activity GROUP BY 2, 3 で調べると、バッチ用ユーザーの接続が60本以上、その大半が idle in transaction でした。バッチがマルチプロセスで動いており、各プロセスがトランザクションを開始したまま外部APIの応答を待っていたのです。合計でmax_connectionsを超え、後から来たWebの接続が拒否されていました。
対処
- PostgreSQL側で
idle_in_transaction_session_timeout = '60s'を設定し、トランザクションを開いたまま放置する接続を強制切断するようにした。 - バッチのコードを修正し、外部API呼び出しをトランザクションの外に出した。
- Webとバッチの間にPgBouncer(transactionモード)を置き、DB側の接続数をユーザーごとに
default_pool_sizeで上限管理するようにした。 - 予備として
superuser_reserved_connections(デフォルト3)があるため、緊急時に管理者だけは接続できることを確認した。
接続数の問題は「誰がどれだけ使っているか」を pg_stat_activity(MySQLなら SHOW PROCESSLIST)で見ることから始まります。設定値を上げる前に、必ず内訳を確認してください。
設定値の目安と判断の手順
小さく始めて計測で増やす
- DB側の同時処理能力の目安を「CPUコア数×2〜4」程度と置く(I/O待ちが多いなら多め)。これがDBに張る接続の合計の上限になる。
- アプリのプロセス総数(ワーカー×台数+バッチ+ワーカー)を数え、各プロセスのプール上限×プロセス数が1の値に収まるか確認する。
- 収まらない、またはサーバーレスで数が読めないなら、PgBouncer/ProxySQL/RDS Proxyを導入し、DB側の接続はプーラーだけが張るようにする。
- プール待ちのタイムアウト(
pool_timeoutなど)は短め(数秒〜30秒)にし、待ちきれない場合はエラーにして早く失敗させる。 pg_stat_activityの接続数と待ち時間を監視し、実測に基づいて増減する。
クエリ自体が遅いために接続が長く占有されているケースも多く、その場合はプール設定よりもSQLパフォーマンスチューニングが先です。
まとめ
コネクションプーリングは「接続を使い回す技術」であると同時に、「DBに同時に流す仕事の量を制御する仕組み」です。
- DB接続は確立も保持も高コスト。max_connectionsを上げるのは解決ではなく先送り。
- アプリ内プールはプロセスごとに独立するので、合計本数を必ず計算する。
- 合計が収まらない・サーバーレスで数が読めないなら、PgBouncer(transactionモード)などの外部プーラーで集約する。
- transactionモードではセッション状態を持つ機能が使えない。ORMのプリペアドステートメント設定に注意。
- 問題が起きたら、まず
pg_stat_activityで内訳を見る。
まずは今のシステムのプロセス総数とプール設定を紙に書き出し、合計本数を確かめてみてください。DBの接続設計やプーラー導入を含むインフラの見直しは、Harmonic Societyのシステム開発・インフラ支援でご相談いただけます。
Harmonic Society
この記事の内容、自社の業務でも活かせそうですか?
ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。
関連記事
Related / 9 articles
Notes & Insights
- プログラミング
DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用
DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。
- プログラミング
WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド
WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。
- プログラミング
セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説
CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。
- プログラミング
クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計
クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。
- プログラミング
秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除
APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。
- プログラミング
開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御
開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。
- プログラミング
ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方
NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。
- プログラミング
ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方
ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。
- プログラミング
localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決
localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。
Harmonic Society
「読んで終わり」にせず、自社の業務で試してみませんか?
AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。
無料・30分・オンラインOK|1営業日以内に返信します