When queries never reach the database. Everything here raises ConnectionError
or TimeoutError, not a constraint error — if you are seeing the latter, the
database is fine and the problem is the data.What prorm retries, and what it does not#
isRetryableError() is the whole policy, and it is deliberately narrow:import { TimeoutError, ResourceLockedError, ConnectionError, ErrorCodes } from 'ts-prorm-orm';
// isRetryableError(), as src/errors/utils.ts defines it:
function isRetryable(error: unknown): boolean {
if (error instanceof TimeoutError) return true; // yes
if (error instanceof ResourceLockedError) return true; // yes
if (error instanceof ConnectionError) {
return (
error.code === ErrorCodes.CONNECTION_TIMED_OUT || // yes
error.code === ErrorCodes.HOST_NOT_REACHABLE // yes
);
}
return false; // everything else: no
}
A refused connection, a failed handshake and a rejected password are not
retried — retrying them would fail identically, and hammering a database that
is refusing you is how a brief outage becomes a long one.[!NOTE]
isRetryableError() classifies an error that has reached your code. Some
dialects also retry their own connect() by matching the error message
against retry.match, and those defaults are wider: the MySQL dialect's
include ECONNREFUSED and ER_ACCESS_DENIED_ERROR, so a refused or
rejected connection is attempted retry.max times before it surfaces. Set
retry.match explicitly if you want the narrow policy at connect time too.
Tuning the policy#
const db = new Prorm({
/* … */
retry: {
max: 3, // attempts (default 3)
timeout: 1000, // ms between attempts (default 1000)
backoff: true, // off by default — turn it on
backoffMultiplier: 2, // 1s, 2s, 4s …
backoffMax: 10000, // ceiling
},
});
Turn backoff on. Fixed-interval retries from every instance at once produce a
thundering herd against a database that is already struggling; exponential
backoff spreads them out.match takes message patterns for driver errors prorm does not classify
itself — Db2's SQL30081N, for instance.The failures, and what each means#
| Symptom | Usually |
|---|
ECONNREFUSED | Nothing listening: wrong port, or the database is down. Not retried. |
ETIMEDOUT / CONNECTION_TIMED_OUT | A network path problem — security group, firewall, or a saturated host. Retried. |
HOST_NOT_REACHABLE | DNS or routing. Retried, but if DNS is wrong it will keep failing. |
| Authentication failed | Credentials changed under you. See Credential rotation. |
too many connections | The server's limit, not the pool's. Lower pool.max × instance count below it. |
| Timeouts while the database is idle | The pool is exhausted, not the engine. |
Pool exhaustion looks like a slow database#
The clearest tell: query latency climbs while database CPU stays flat. Requests
are waiting for a connection, not for an answer.import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'postgres',
pool: {
max: 20,
acquire: 30000, // how long to wait for a free connection
idle: 10000,
evict: 1000,
},
});
Do not reach for enableEvents or logPoolOperations here. They report on a
bookkeeping pool that queries never check a connection out of, not on the
driver's pool, so they print nothing useful while real connections leak, and
getPoolStats() has the same blind spot. Count connections from the database
side: pg_stat_activity on PostgreSQL (a leak shows as idle in transaction),
SHOW PROCESSLIST and information_schema.innodb_trx on MySQL and MariaDB.Two causes, in order of likelihood:1.
A leak. A transaction that threw before commit or rollback holds its
connection until acquire gives up. Always release in a finally, or use
the callback form of transaction(), which does it for you.
2.
Genuinely too small. pool.max multiplied by the number of application
instances must stay under the server's connection limit — and leave room for
migrations and your own psql session.
Checking connectivity from the process that is failing#
Resolve and reach the host#
From a shell on the failing instance, not your laptop:$ getent hosts db.internal.example.com
10.0.4.17 db.internal.example.com
$ nc -vz db.internal.example.com 5432
Connection to db.internal.example.com 5432 port [tcp/postgresql] succeeded!
No address is DNS; a timeout is the network path; Connection refused is
nothing listening on that port.Authenticate through prorm#
import { formatError } from 'ts-prorm-orm';
try {
await db.authenticate();
console.log('reachable');
} catch (err) {
console.error('unreachable:', formatError(err));
}
If the host answers but this fails, the problem is TLS or credentials, not
the network.Compare with a healthy instance#
Run the same two steps from an instance that is working. Most "the database
is down" incidents are one instance's networking, not the database.Failover — when the connection drops because the host moved Modified at 2026-10-04 00:40:16