Safety

Don't wrap in production

slowdep adds artificial latency. In production, this means real users wait longer for no benefit. Always gate the wrap on an environment check.

import { withLatency } from 'slowdep'

// The conditional wrap pattern — zero cost in production
const queryUser = process.env.NODE_ENV !== 'production'
  ? withLatency(dbQueryUser, 'postgres')
  : dbQueryUser

For a reusable pattern that centralizes the check, see Production usage which covers a maybeSlowDep helper and tree-shaking options.

Architecture

Wrap at the boundary

Wrap the function that directly calls the external service — not a function several layers up the call stack. Wrapping too high obscures which dependency is slow and makes it impossible to give different services different latency profiles.

Too high — wraps everything at once

Wrapping processOrder mixes together database, payment, and email latency into one opaque number. You can't tune them individually.

// Too high: processOrder calls db + stripe + email internally
const slowProcessOrder = withLatency(processOrder, 'http')
At the boundary — each dependency independently

Wrap each client or adapter function with the preset that matches the real service.

// At the boundary: each dep gets its own realistic profile
const slowQuery   = withLatency(db.query,          'postgres')
const slowCharge  = withLatency(stripe.charge,      'stripe')
const slowEmail   = withLatency(mailer.send,        'http')
Testing resilience

Test the failure path

The most valuable thing slowdep does is let you test code paths that are hard to trigger against a real service. Set errorRate high temporarily — even to 0.5 — to force your retry and fallback logic to execute on every test run.

// In a specific resilience test: set errorRate to 50%
// Every other call fails — retry logic runs every time
const flakyCharge = withLatency(createCharge, {
  p50: 200,
  p99: 2000,
  errorRate: 0.5
})

test('retries and eventually succeeds', async () => {
  const result = await withRetry(flakyCharge, { maxAttempts: 5 })
  expect(result).toBeDefined()
})

Use a high error rate only in tests specifically targeting resilience. Leave it at the preset default (or lower) for integration tests that are testing normal behavior.

CI strategy

Use realistic presets for integration tests

Zero-latency tests (no slowdep, or slowdep mocked away) catch logic bugs. Realistic-latency integration tests catch timeout, retry, and concurrency bugs that zero-latency tests never trigger.

Consider a two-tier test setup:

  • Unit tests: Mock withLatency away entirely. Fast, deterministic, good for logic coverage.
  • Integration tests: Enable slowdep with realistic presets. Slower, but catches timeout misconfiguration, insufficient retry budgets, and cascade failure bugs before they reach production.
// jest.config.integration.js
export default {
  testMatch: ['**/*.integration.test.ts'],
  testEnvironmentOptions: {
    // No SLOWDEP_BYPASS — latency is active
    env: { NODE_ENV: 'test' }
  },
  testTimeout: 30000 // higher timeout for realistic latency
}
Scope

Don't use slowdep as a load testing tool

slowdep delays individual calls, one at a time. It is not a load generator, a traffic simulation tool, or a concurrency stress tester. It answers the question "what happens when a single call takes longer than expected?" — not "what happens when 1,000 users hit my service simultaneously?"

For load testing and concurrency simulation, use dedicated tools like k6, Artillery, or wrk. slowdep and load testing are complementary: use slowdep to validate your resilience patterns are correct, then use a load tester to validate they scale.

Right tool for the job

slowdep answers: "Does my code handle a slow dependency correctly?" Load testers answer: "Does my system handle many concurrent users?" Use both.

Configuration

Pick p99 carefully

The 99th percentile is the boundary between "normal variance" and "something is wrong." If your timeout is shorter than the p99 of a dependency, 1% of calls will time out even during normal operation. In a service processing 100 requests per second, that's one timeout per second — a constant stream of errors.

// Bad: timeout of 100ms with postgres p99 of 200ms
// 1% of calls will time out in normal operation
const QUERY_TIMEOUT = 100  // too short
const slowQuery = withLatency(query, 'postgres') // p99: 200ms

// Better: timeout >= 3-5× p99 to account for the tail
const QUERY_TIMEOUT = 1000  // comfortable above p99

The rule of thumb: set your timeout to at least 3–5× the p99 of the dependency. For databases (p99 ~200ms), a 1s timeout is reasonable. For LLM APIs (p99 ~7–8s), you need 30s or more. slowdep surfaces the mismatch during testing rather than production.