エラーの概要
アプリがデータベース接続を開こうとすると、次のエラーが発生します:
FATAL: too many connections for role "myapp"
または、このような別のパターンも:
FATAL: remaining connection slots are reserved for non-replication superuser connections
どちらも根本原因は同じです。RDSが接続数の上限に達し、それ以上の接続を拒否しています。
根本原因
RDS上のPostgreSQLは、インスタンスのメモリに基づいて同時接続数に厳格な上限を設けています。計算式は LEAST({DBInstanceClassMemory/9531392}, 5000) です。db.t3.micro の場合、合計で約60〜80接続しかありません。かなり少ない数です。
この上限に達すると、既存の接続のほとんどがアイドル状態であっても、新しい接続の試みはすべて失敗します。
よくある原因:
- コネクションプーラーなし — 各スレッドまたはプロセスが独自の接続を保持する
- Lambdaのコールドスタート — 新しい実行ごとに既存の接続を再利用せず新しい接続を開く
- 接続リーク — 開いたまま閉じられない接続(エラー処理のパスに潜んでいることが多い)
- 複数の環境が同一のRDSインスタンスを共有している(開発・ステージング・本番)
- 多数のレプリカでORMプールが適切に設定されていない — 10レプリカ × 各20接続 = 200接続、あっという間
ステップ1:現在の接続使用状況を確認する
修正の前に、実際に何が起きているか確認しましょう。スーパーユーザーまたは rds_superuser として接続し、以下を実行します:
SELECT count(*), usename, state
FROM pg_stat_activity
GROUP BY usename, state
ORDER BY count DESC;
上限と比較します:
SELECT current_setting('max_connections') AS max,
count(*) AS current
FROM pg_stat_activity;
アイドル接続が多数あって上限に近い場合は、余裕を作るために不要な接続を強制終了します:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND usename = 'myapp'
AND query_start < now() - interval '10 minutes';
修正1:pgBouncerを追加する(推奨)
pgBouncerはアプリとRDSの間に配置します。アプリはpgBouncerに対して何百もの「接続」を開きます。pgBouncerはそれらを少数の実際のRDS接続にまとめます — たとえば、1000のアプリ接続を20のRDS接続にマッピングします。
EC2インスタンス上またはサイドカーコンテナとしてデプロイします。最小限の設定(/etc/pgbouncer/pgbouncer.ini):
[databases]
mydb = host=mydb.xxxx.us-east-1.rds.amazonaws.com port=5432 dbname=mydb
[pgbouncer]
listen_port = 5432
listen_addr = 0.0.0.0
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 600
userlist.txt の内容:
"myapp" "md5hashofpassword"
アプリの接続先をRDSから直接ではなく、pgBouncerのアドレスに変更します。これで完了です — アプリ接続1000本、実際のRDS接続は20本。
Lambdaを使用している場合は、代わりにRDS Proxyを使用してください。これはAWSのマネージドプーラーで、pgBouncerプロセスを常駐させられないサーバーレス環境向けに設計されています:
aws rds create-db-proxy \
--db-proxy-name myapp-proxy \
--engine-family POSTGRESQL \
--auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:...","IAMAuth":"DISABLED"}]' \
--role-arn arn:aws:iam::123456789:role/rds-proxy-role \
--vpc-subnet-ids subnet-xxx subnet-yyy \
--vpc-security-group-ids sg-xxx
修正2:パラメータグループでmax_connectionsを増やす
一時的な緩和策であり、根本的な解決策ではありません。カスタムパラメータグループで上限を引き上げます:
aws rds modify-db-parameter-group \
--db-parameter-group-name myapp-params \
--parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot"
パラメータグループを適用して再起動します:
aws rds modify-db-instance \
--db-instance-identifier mydb \
--db-parameter-group-name myapp-params \
--apply-immediately
aws rds reboot-db-instance --db-instance-identifier mydb
プーラーを追加せずにデフォルトの2倍を超えないようにしてください。PostgreSQLは接続ごとに共有メモリを割り当てます — 上げすぎると接続エラーの代わりにOOMクラッシュが起きます。
修正3:ORMのコネクションプールを調整する
SQLAlchemyのデフォルト設定はRDSには寛大すぎることが多いです。適切に絞り込みましょう:
from sqlalchemy import create_engine
engine = create_engine(
DATABASE_URL,
pool_size=5, # プロセスごとの永続接続数
max_overflow=10, # 一時的なバースト接続数
pool_timeout=30, # エラーを発生させるまでの待機秒数
pool_pre_ping=True, # 使用前に古い接続を破棄する
pool_recycle=1800, # 30分後に接続を再利用する
)
計算式が重要です:(pool_size + max_overflow) × レプリカ数 = RDSの合計接続数。10レプリカでこの設定だと150接続になります。インスタンスサイズをそれに合わせて選んでください。
pgを使用したNode.js:
const { Pool } = require('pg');
const pool = new Pool({
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
修正4:Lambdaの接続リークを対処する
Lambdaの実行モデルが落とし穴です。コールドスタートのたびに新しい接続が作られます。ハンドラー内で接続を開くと、呼び出しごとに新しい接続が作られ、どれもきれいに閉じられないリスクがあります。
ウォーム実行で再利用されるよう、接続をハンドラーの外に移動します:
import psycopg2
import os
# ハンドラーの外 — ウォーム実行をまたいで永続する
conn = None
def get_connection():
global conn
if conn is None or conn.closed:
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
)
return conn
def handler(event, context):
db = get_connection()
# dbを使用...
これは効果がありますが、あくまで応急処置です。高い並行性の場合、数百のLambdaインスタンスがそれぞれ独自の接続を保持します。RDS Proxy(修正1)を使えばより根本的に解決でき、ハンドラーの変更も不要です。
確認方法
修正を適用した後、接続数が安定するのを監視します:
-- 数秒ごとに実行してリアルタイムで監視
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
pgBouncerが動作している場合は、統計パネルを確認します:
psql -h localhost -p 5432 -U pgbouncer pgbouncer -c 'SHOW POOLS;'
cl_active(クライアント接続数)と sv_active(サーバー接続数)を比較します。200のクライアント接続が18のサーバー接続に多重化されていれば、pgBouncerが正常に機能しています。
予防策
- 常に接続プールを使用する — 常駐サービスにはpgBouncer、Lambda/サーバーレスにはRDS Proxy
- 古い接続がエラーを引き起こす前に破棄するため、
pool_pre_ping=True(SQLAlchemy)または同等の設定を有効にする - スタックしたトランザクションを自動終了するために
idle_in_transaction_session_timeoutを設定する:SET idle_in_transaction_session_timeout = '5min'; DatabaseConnectionsに CloudWatchアラームを追加する — 100%ではなくmax_connectionsの80%でアラートを設定するdb.t3.microで複数のサービスを本番稼働させない — 60〜80接続の上限がすぐに問題になる

