Connection timeout is a name shared by at least four distinct failures, and they have almost nothing in common: the connect timeout, the read timeout, pool acquisition, and idle connection reaping.
Treating them as one problem is why these take so long to diagnose. A connect timeout means packets are not arriving and you should look at firewalls and routing. A read timeout means the connection is fine and the other end is slow, so you should look at queries and load. Chasing a firewall rule when the real problem is an unindexed query is a long afternoon.
The good news is that separating them is usually a single measurement.
Table of contents
- The four timeouts
- Splitting connect from read in thirty seconds
- Connect timeouts: dropped, not refused
- Pool exhaustion, which masquerades as a network problem
- Idle connections and the immediate failure
- How this fits the rest of the stack
- FAQ
The four timeouts
Each fires at a different stage and points somewhere different.
Connect timeout. The TCP handshake did not complete. Nothing was sent because no connection exists. Causes: firewall dropping packets, nothing listening on the port, wrong host or port, DNS resolving to a stale address, or routing failure.
Read or socket timeout. The connection was established and the request sent, but the response did not arrive in time. The other end is alive and slow. Causes: a slow query, an overloaded server, a lock, or a genuinely long operation.
Pool acquisition timeout. Your application waited for a connection from its own pool and none became available. The remote server may be perfectly healthy. Causes: pool too small, connections leaked, or every connection busy on slow queries.
Idle timeout. A connection sat unused and something closed it, but your pool still believes it is open. The next request to use it fails immediately rather than slowly. Causes: a database idle timeout, a load balancer or firewall reaping idle connections, shorter than your pool’s own lifetime setting.
The distinguishing measurement is time-to-failure. A connect timeout fails at exactly your configured connect timeout. A read timeout fails at the read timeout. An idle-connection failure fails immediately, which is the giveaway, because a broken pipe error arriving in two milliseconds is not a network reachability problem.
Splitting connect from read in thirty seconds
One curl distinguishes the first two definitively, because it reports the time spent in each phase.
curl -sS -o /dev/null -w \
'dns: %{time_namelookup}s connect: %{time_connect}s tls: %{time_appconnect}s ttfb: %{time_starttransfer}s total: %{time_total}s\n' \
https://example.com/slow/path
If connect never completes, it is a connect timeout and the problem is below the application. If connect is fast and ttfb is enormous, the connection is fine and the server is slow.
For a database rather than HTTP, test the port directly first:
# Can you even open a socket? Times out on a firewall drop.
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/db-host/5432' && echo open || echo blocked
# And check what the name resolves to, in case it is stale.
getent hosts db-host
That DNS check catches a surprisingly common case: the database moved, the record was updated, and something in the path is holding a cached answer pointing at an address that no longer accepts connections.
Connect timeouts: dropped, not refused
There is an important distinction inside connect failures, and the error text tells you which you have.
Connection refused is immediate. Something actively sent a reset, which means the host is reachable and nothing is listening on that port. Check whether the service is running and what address it is bound to.
Connection timed out is slow. Packets are being silently discarded, which is what a firewall DROP rule does. Nothing rejected you; nothing answered.
# Is the service listening, and on which address?
sudo ss -tlnp | grep 5432
# 0.0.0.0 or :: means all interfaces.
# 127.0.0.1 means local only, which refuses everything from outside.
That binding check is worth doing early. A database bound to localhost accepts local connections perfectly and refuses every remote one, which looks exactly like a firewall problem from the client’s side and is fixed in a config file rather than a firewall.
For a managed database, the equivalent is the network access list. A connection from an address not on the allow list is typically dropped rather than refused, producing the slow timeout. The database network security docs cover private networking on RunxBuild, which sidesteps the whole category by not exposing the database publicly at all.
Pool exhaustion, which masquerades as a network problem
This is the one that most often gets misdiagnosed, because the error frequently says timeout and people go looking at the network.
The shape: your application holds a pool of connections. Under load, all of them are busy. New requests queue waiting for one, and after the acquisition timeout they fail. The database is idle. The network is fine. Nothing is wrong except that your pool is smaller than your concurrency.
The tell is that the database shows few active connections while your application reports timeouts. Check both sides at once:
-- Postgres: what is actually connected, and what is it doing?
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;
-- Long-running queries, which are usually the real cause.
SELECT pid, now() - query_start AS duration, state, left(query, 80)
FROM pg_stat_activity
WHERE state <> 'idle' AND now() - query_start > interval '5 seconds'
ORDER BY duration DESC;
Nine times out of ten the fix is not a bigger pool. A pool exhausted by slow queries will exhaust a larger pool slightly later while putting more load on the database. Find the slow query and index it.
The genuine sizing case is different: instances multiplied by pool size must stay under the database’s connection limit. Scaling from two instances to fifty with a pool of ten asks for five hundred connections, and most plans will refuse. Cap maximum instances, keep per-instance pools small, and check the arithmetic against the connection limits documentation before a launch rather than during one.
Idle connections and the immediate failure
The signature here is distinctive: the error arrives instantly rather than after a timeout, often as a broken pipe or a reset, and it usually happens on the first request after a quiet period.
What has occurred is that something closed the connection while your pool was not looking. The database has its own idle timeout. Load balancers and stateful firewalls reap idle flows, frequently after a few minutes, often without sending anything to either end.
The rule that fixes it: your pool’s maximum connection lifetime must be shorter than the shortest idle timeout anywhere in the path. If the load balancer reaps at 350 seconds, retire connections at 240.
# The three settings, whatever your pool library calls them:
max_lifetime = 240s # retire connections before anything else closes them
idle_timeout = 120s # release connections nobody is using
validate_on_borrow = true # cheap liveness check before handing one out
Validation on borrow is the safety net. It costs a trivial round trip and converts a request that would have failed on a dead connection into one that transparently gets a fresh one. On a busy service the cost is negligible relative to what it prevents.
TCP keepalives are the other half, and they need to be set low enough to matter. The default is often two hours, which is far longer than any reaper’s patience and therefore useless for this purpose.
How this fits the rest of the stack
Most timeout investigations end at capacity: the pool was too small, the query was too slow, or the plan was too small for the concurrency. The RunxBuild hosting calculator makes the headroom question concrete by putting service plans and database plans side by side, with the connection ceiling visible against the plan rather than discovered during the traffic that exhausted it.
Useful related references:
- The 504 Gateway Timeout Error, in Plain English
- cURL Error 28: Which Timeout Fired and Why It Matters
- HTTP 504 Gateway Timeout: Reading the Error as a Map of Your Stack
- Database connection limits on RunxBuild
FAQ
What is the difference between a connect timeout and a read timeout?
A connect timeout means the TCP handshake never completed, so nothing was sent and the cause is below your application: a firewall, a wrong address, or nothing listening. A read timeout means the connection succeeded and the request was sent, but the response was too slow, which points at load or a slow query.
Why do I get connection timeouts when my database is idle?
Almost certainly pool exhaustion in your application rather than a database problem. All pooled connections are busy, new requests queue, and they fail at the acquisition timeout while the database itself has capacity to spare. Look for slow queries holding connections rather than raising the pool size.
What causes an immediate connection reset rather than a slow timeout?
A connection your pool believes is open that something else has already closed. Database idle timeouts, load balancers and stateful firewalls all reap idle connections, often silently. Set your pool’s maximum connection lifetime shorter than the shortest idle timeout in the path, and validate connections before handing them out.
Should I increase my connection pool size?
Usually not as a first response. A pool exhausted by slow queries will exhaust a larger pool slightly later while adding load to the database. Fix the slow query first. Increase the pool only when concurrency genuinely exceeds it, and check that instances multiplied by pool size stays under the database connection limit.
Why does connection refused differ from connection timed out?
Refused is immediate and means something sent a reset: the host is reachable and nothing is listening on that port. Timed out is slow and means packets are being silently discarded, which is what a firewall drop rule does. The first is usually a service or binding problem, the second a network or firewall one.