postgres latency profile
The 'postgres' preset models a well-tuned Postgres instance on the same network. Latency follows a lognormal distribution — most queries are fast, but a tail of slow queries appears at realistic frequency.
Real Postgres latency is not uniformly distributed. Most queries hit the buffer cache and return in 1–3ms. Complex queries, lock waits, and autovacuum interference push the tail to 200ms+. The lognormal distribution matches this shape accurately.
Wrap pool.query in two lines
The most direct approach: wrap pool.query so every query in your service goes through realistic latency. Use this in your test setup file.
import { Pool } from 'pg'; import { withLatency } from 'slowdep'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Wrap pool.query with realistic Postgres latency pool.query = withLatency(pool.query.bind(pool), 'postgres'); // Now every call to pool.query() will experience realistic latency const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
Always use pool.query.bind(pool), not just pool.query. Without binding, this inside the pg Pool method will be undefined and you'll get a runtime error.
Wrap the entire pool with withLatencyAll
Use withLatencyAll to wrap every async method on the pool at once — query, connect, and others. This is the most thorough approach.
import { Pool } from 'pg'; import { withLatencyAll } from 'slowdep'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Wrap every async method on pool in one call const slowPool = withLatencyAll(pool, 'postgres'); // pool.query, pool.connect, etc. all now have realistic latency const { rows } = await slowPool.query('SELECT id, email FROM users LIMIT 50'); // Acquiring a connection also has realistic pool-checkout latency const client = await slowPool.connect(); try { await client.query('BEGIN'); await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]); await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]); await client.query('COMMIT'); } finally { client.release(); }
Wrap specific query functions
When you have repository or data-access functions, wrap them individually. This keeps slowdep out of your production Pool configuration and makes it easy to toggle per function.
import { Pool } from 'pg'; import { withLatency } from 'slowdep'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Repository functions — wrap each data-access function individually async function findUserByEmail(email: string) { const { rows } = await pool.query( 'SELECT * FROM users WHERE email = $1', [email] ); return rows[0] ?? null; } async function insertUser(email: string, name: string) { const { rows } = await pool.query( 'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *', [email, name] ); return rows[0]; } // Wrap the repository functions, not the pool export const slowFindUserByEmail = withLatency(findUserByEmail, 'postgres'); export const slowInsertUser = withLatency(insertUser, 'postgres');
Jest test: query timeout and retry
The most important thing to verify is that your application correctly handles slow or failed queries. Use slowdep to drive both scenarios deterministically in Jest.
import { withLatency } from 'slowdep'; import { getUserById } from '../src/users'; // Simulate a very slow Postgres query — p99 of a bad day const slowQuery = withLatency( jest.fn().mockResolvedValue({ rows: [{ id: 1, name: 'Alice' }] }), { p50: 300, p99: 1200 } // simulate a stressed database ); test('getUserById times out after 500ms', async () => { await expect( Promise.race([ getUserById(1, { queryFn: slowQuery }), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 500) ) ]) ).rejects.toThrow('timeout'); }, 3000);
import { withLatency } from 'slowdep'; // errorRate: 1.0 forces every call to fail — great for retry logic tests const alwaysFails = withLatency( jest.fn().mockRejectedValue(new Error('Connection reset by peer')), { p50: 5, p99: 200, errorRate: 1.0 } ); test('retries up to 3 times on connection error', async () => { const mockQuery = jest.fn() .mockRejectedValueOnce(new Error('Connection reset')) .mockRejectedValueOnce(new Error('Connection reset')) .mockResolvedValueOnce({ rows: [{ id: 42 }] }); const slowMock = withLatency(mockQuery, 'postgres'); const result = await queryWithRetry(slowMock, 'SELECT 1', { maxRetries: 3 }); expect(result.rows[0].id).toBe(42); expect(mockQuery).toHaveBeenCalledTimes(3); });
import { Pool } from 'pg'; import { withLatencyAll } from 'slowdep'; test('pool exhaustion: 11th concurrent query waits for a slot', async () => { const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 }); const slow = withLatencyAll(pool, 'postgres'); // Fire 11 concurrent queries against a pool of 10 const queries = Array.from({ length: 11 }, (_, i) => slow.query('SELECT pg_sleep(0.1), $1::int AS n', [i]) ); const start = Date.now(); const results = await Promise.all(queries); const elapsed = Date.now() - start; expect(results).toHaveLength(11); // 11th query had to wait for a slot, so total > 200ms expect(elapsed).toBeGreaterThan(200); await pool.end(); });
Only enable in development and test
Never run slowdep in production. Use a conditional wrap that checks NODE_ENV before applying latency. This pattern is zero-overhead in production — the condition is evaluated once at startup.
import { Pool } from 'pg'; import { withLatencyAll } from 'slowdep'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Zero overhead in production — condition evaluated once at startup export const db = process.env.NODE_ENV !== 'production' ? withLatencyAll(pool, 'postgres') : pool; // Use db.query everywhere in your service const { rows } = await db.query('SELECT * FROM orders WHERE user_id = $1', [uid]);
You can also gate on a dedicated env var like SLOWDEP_ENABLED=true in .env.development and .env.test. This lets you disable slowdep even in development when you need fast iteration without changing NODE_ENV.
Scenarios this integration helps you verify
- Query timeout calibration — does your 500ms timeout actually fire before the request handler returns a 504?
- Connection pool exhaustion — when all 10 pool connections are busy, does the 11th request queue or error?
- Transaction rollback on slow commit — if COMMIT takes 200ms and your request budget is 300ms, does the partial transaction get cleaned up?
- N+1 query detection — realistic per-query latency makes N+1 patterns immediately visible in test timing output
- Retry logic correctness — set
errorRate: 1.0on a mock to force failures and verify your retry handler respects backoff - Circuit breaker thresholds — drive the p99 tail to trigger your circuit breaker, then verify it opens after the right number of failures
- Slow query logging — verify your observability layer logs queries that exceed a threshold
Fully typed integration
slowdep preserves the return type of the wrapped function. The wrapped pool.query is still typed as returning Promise<QueryResult> — no type casting needed.
import { Pool, QueryResult } from 'pg'; import { withLatency, withLatencyAll } from 'slowdep'; const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Type is inferred: (text: string, values?: any[]) => Promise<QueryResult> const slowQuery = withLatency(pool.query.bind(pool), 'postgres'); interface User { id: number; email: string; created_at: Date; } async function findUsers(): Promise<User[]> { // QueryResult<User> — fully typed, no casting const result: QueryResult<User> = await slowQuery( 'SELECT id, email, created_at FROM users ORDER BY created_at DESC' ); return result.rows; }