Your connection pool is probably wrong in one of two directions
ActiveRecord::ConnectionTimeoutError and "too many connections" are the same mistake made in opposite directions. The arithmetic is simple.
Two errors, one root cause. ActiveRecord::ConnectionTimeoutError means threads are
queueing for a connection that the pool will not give them. FATAL: too many
connections means you asked the database for more than it will serve.
Both come from not doing a small piece of arithmetic.
The pool is per process
This is the detail that catches people. pool: 5 in database.yml does not mean five
connections. It means five *per Puma worker*, plus five per Sidekiq process, plus any
other process that boots your app.
Four web workers and two job processes at pool: 5 is thirty connections, from one
machine, before you have scaled anything.
Sizing it from the top down
Start with the database's own limit. Managed Postgres instances have a connection ceiling that is frequently much lower than people assume on small plans — low enough that a modest deployment can exhaust it.
Then subtract headroom for migrations, consoles, monitoring and your own psql session. Divide what remains by the number of processes you will run. That is your ceiling per process.
Then size the pool to what the process can actually use: at least as many connections as it has threads. A Puma worker with three threads needs at least three, or threads will block on each other for no reason. A Sidekiq process needs at least its concurrency setting.
Pool smaller than thread count is the direct cause of connection timeouts under load, and it is the most common misconfiguration I find.
Pool size is per process. Multiply by every process you run before comparing it to your database's limit.
When multiplication stops working
Past a certain scale the arithmetic stops resolving — you need more processes than the database has connections. That is what a connection pooler is for. It sits between your app and the database and multiplexes many client connections onto few server ones.
In transaction pooling mode, a server connection is held only for the duration of a
transaction, which is enormously more efficient. The trade is that anything relying on
session state across transactions — prepared statements, advisory locks, SET — needs
care, and Rails has settings for exactly this.
Leaks and long checkouts
A connection held longer than needed is a connection nobody else can use. Two patterns cause it.
Threads spawned manually inside a request that check out a connection and never return it. If you spawn a thread that touches the database, wrap the work so the connection is returned when it finishes.
And long-running work inside a request — a slow external HTTP call between two queries holds the connection for the entire round trip while doing nothing with it. Move that work to a job, or restructure so the connection is released first.
The check worth running today
Multiply pool size by process count. Compare it to your database's connection limit. If the first number is larger, you have an incident waiting for your next traffic spike.
If pool size is smaller than thread count, you have latency you are currently blaming on the database.