Retry & Pooling

Retry behaviour for read-only statements, pool statistics and health checks

Retry configuration

typescript
export interface DBConfig {
type: DBType;
connectionString: string;
retryAttempts?: number; // default 3
retryDelay?: number; // default 1000 (ms)
maxJitter?: number; // default 100 (ms)
}

Fields:

  • retryAttempts Number of attempts for read-only statements. Default 3. Set to 1 to effectively disable retry.
  • retryDelay Base delay in milliseconds. Default 1000.
  • maxJitter Upper bound of the random jitter added to each backoff, in milliseconds. Default 100.
example/retry-config.ts
const orm = new Stabilize({
type: DBType.Postgres,
connectionString: process.env.DATABASE_URL!,
retryAttempts: 5,
retryDelay: 500,
maxJitter: 100,
});

Backoff formula

The delay before each attempt is exponential with a random jitter added:

typescript
retryDelay * Math.pow(2, attempt - 1) + getJitter()
// where
getJitter = () => Math.random() * this.maxJitter

The multiplier is always 2 and is not configurable. There are no initialDelay or maxDelay options, and no configurable list of retryable error codes.

Retry applies to reads only

typescript
const attempts = isReadOnlyStatement(query) ? this.retryAttempts : 1;

A statement is treated as read-only when, after leading whitespace and comments are stripped, it begins with one of these prefixes:

typescript
["SELECT", "PRAGMA", "SHOW", "EXPLAIN", "VALUES"]

Every write statement runs exactly once, always — there is no retry of write statements.

No connection pool configuration

There is no pool configuration. There are no max, min, idleTimeout or connectionLimit fields. Every driver object is built from connectionString alone:

typescript
// MySQL
this.client = mysql.createPool(config.connectionString);
// Postgres
this.client = new Pool({ connectionString: config.connectionString! });
// SQL Server
this.client = new sql.ConnectionPool(config.connectionString);
// SQLite — a file handle, not a pool
this.client = new Database(config.connectionString, { create: true });

The SQL Server pool is built but left unconnected: connect() is async and the constructor is not, so the pool is opened lazily on the first statement instead.

poolStats()

typescript
async poolStats(): Promise<{ active: number; idle: number; total: number }>

Returns pool counters. The shape is the same for every driver, but the values are produced differently:

typescript
// Postgres — active is hardcoded 0, not measured
{ active: 0, idle: 0, total: raw.totalCount }
// MySQL
{
active: raw._allConnections.length,
idle: raw._freeConnections?.length ?? 0,
total: raw._allConnections.length,
}
// SQL Server — read through the DBClient wrapper one level down
{
active: pool.borrowed ?? 0,
idle: pool.available ?? 0,
total: pool.size ?? 0,
}
// SQLite — no pool; sentinels meaning "not applicable", not an error
{ active: -1, idle: -1, total: -1 }
example/pool-stats.ts
const stats = await orm.poolStats();
console.log(stats.active, stats.idle, stats.total);

healthCheck() — Stabilize

typescript
async healthCheck(): Promise<{ status: string; database: string; latencyMs: number; cacheStatus: string }>

status is "healthy" or "unhealthy". database is the DBType. cacheStatus names the backend rather than reducing it to connected-or-not: "disabled" when enabled was false, "in-memory" when there is no redisUrl and the cache is confined to this process, "connected" or "connected (miss)" for a Redis client that did or did not find its probe key, and "unknown" when the check threw.

example/health-orm.ts
const health = await orm.healthCheck();
// { status: "healthy", database: "sqlite", latencyMs: 0.5, cacheStatus: "disabled" }

healthCheck() — Repository

typescript
async healthCheck(): Promise<{ status: string; table: string; rows: number; latencyMs: number }>

The repository variant reports on a single table. rows is -1 when the row count failed.

example/health-repo.ts
const health = await userRepo.healthCheck();
// { status: "healthy", table: "users", rows: 128, latencyMs: 0.4 }