The conditional wrap pattern
The simplest approach: a ternary at the point of wrapping. When NODE_ENV is 'production', the original function is returned unchanged. No overhead, no import side effects.
import { withLatency } from 'slowdep' async function queryUser(id: number) { return db.query('SELECT * FROM users WHERE id = $1', [id]) } // Zero overhead in production — returns queryUser directly export const getUser = process.env.NODE_ENV !== 'production' ? withLatency(queryUser, 'postgres') : queryUser
This pattern is evaluated once at module load time, not on every call, so there's no per-request branching cost in production.
Most deployment platforms set NODE_ENV=production automatically. If yours does not, set it explicitly in your deployment configuration. Never rely on the default — if NODE_ENV is unset or 'development' in production, slowdep will be active.
Environment variable control
For more explicit control, use a dedicated environment variable instead of relying on NODE_ENV. This lets you enable slowdep in staging while keeping it off in other non-development environments.
import { withLatency } from 'slowdep' const SLOWDEP_ENABLED = process.env.SLOWDEP_ENABLED === 'true' export const queryUser = SLOWDEP_ENABLED ? withLatency(dbQueryUser, 'postgres') : dbQueryUser
Set SLOWDEP_ENABLED=true in your development and staging environment configs. Leave it unset or set to false in production. This makes the behavior explicit and auditable — anyone reading the deployment config can see whether slowdep is active.
# .env.development SLOWDEP_ENABLED=true # .env.staging SLOWDEP_ENABLED=true # .env.production (or omit entirely) SLOWDEP_ENABLED=false
Wrapper helper
For codebases with many wrapped functions, centralize the environment check in a single helper. This avoids repeating the conditional at every call site and makes it trivial to change the activation logic in one place.
// lib/slowdep.ts — your central wrapper module import { withLatency, withLatencyAll } from 'slowdep' import type { LatencyProfile } from 'slowdep' const enabled = process.env.NODE_ENV !== 'production' && process.env.SLOWDEP_ENABLED !== 'false' export function maybeSlowDep< T extends (...args: any[]) => Promise<any> >(fn: T, profile: LatencyProfile): T { return enabled ? withLatency(fn, profile) : fn } export function maybeSlowDepAll<T extends object>( client: T, profile: LatencyProfile ): T { return enabled ? withLatencyAll(client, profile) : client }
// In your service modules — import from your wrapper import { maybeSlowDep, maybeSlowDepAll } from '../lib/slowdep' export const queryUser = maybeSlowDep(dbQueryUser, 'postgres') export const redis = maybeSlowDepAll(redisClient, 'redis')
Testing that the bypass works
If you want to verify that production builds don't include slowdep at all — not just that the conditional prevents it from being called — you have two options:
Tree shaking with bundlers
Modern bundlers (esbuild, Rollup, webpack 5) tree-shake unused imports. If you use a dynamic import pattern gated on NODE_ENV, the bundler can elide the import entirely in production builds:
// Dynamic import — bundler can eliminate in production const { withLatency } = process.env.NODE_ENV !== 'production' ? await import('slowdep') : { withLatency: (fn: any) => fn }
Bundle analysis
Use your bundler's bundle analyzer (webpack-bundle-analyzer, rollup-plugin-visualizer, or esbuild --metafile) to verify slowdep does not appear in the production bundle. If you see it, check that your NODE_ENV is correctly set at build time, not just at runtime.
Automated test
Add a test to your CI pipeline that sets NODE_ENV=production and asserts that require.resolve('slowdep') is not called or that the production bundle size does not include slowdep:
// bundle-check.test.ts it('production bundle does not include slowdep', async () => { const bundle = await fs.promises.readFile('dist/bundle.js', 'utf8') expect(bundle).not.toContain('Simulated transient error') })
slowdep in staging environments
Staging is one of the most valuable places to run slowdep. Unlike unit tests — where you know what you're testing — staging runs your full application against real workflows with simulated latency. Bugs that require a specific sequence of API calls under load are much more likely to surface here.
Specifically, staging with slowdep can surface:
- Timeout misconfiguration — API handlers with a 1s timeout and a Stripe p99 of 2s will fail 1% of the time in staging, not just in theory.
- Missing retries — features that call external APIs but don't retry transient errors will produce visible failures in staging that are easy to find and fix.
- Cascade problems — if service A calls service B which calls C, and C is slow, does A's timeout fire before B's? Staging shows you the actual failure mode.
- UI/UX under latency — loading states, skeleton screens, and error messages that are only revealed when operations take more than 500ms are much easier to test with slowdep active.
Enable slowdep in staging with the default presets. Use a custom profile only if you have production data showing the presets significantly underestimate your actual latency.