Preset

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.

presetp50p95p99errorRate
postgres 5ms 80ms 200ms 0.1%
Why lognormal?

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.

Quick start

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]);
Bind matters

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.

Wrapping a client

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();
}
Individual wrapping

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');
Testing

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();
});
Production safety

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]);
Tip

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.

What to test

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.0 on 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
TypeScript

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;
}