Preset

anthropic latency profile

The 'anthropic' preset reflects real-world Claude latency for typical prompt lengths. Claude's time-to-first-token is fast, but total latency grows with output length. The 0.5% error rate captures API errors and rate limits.

presetp50p95p99errorRate
anthropic 600ms 3.5s 7s 0.5%
Streaming vs non-streaming latency

slowdep delays the start of the API call — the initial request latency. For streaming responses (client.messages.stream()), slowdep delays when the stream begins, not the individual token delivery. This accurately models network latency and time-to-first-token, which is the most impactful latency for user experience.

Quick start

Wrap messages.create

Wrap client.messages.create — the primary method for non-streaming Claude requests. Use .bind(client.messages) to preserve the internal context.

import Anthropic from '@anthropic-ai/sdk';
import { withLatency } from 'slowdep';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Wrap messages.create — bind to preserve internal context
const slowCreate = withLatency(
  client.messages.create.bind(client.messages),
  'anthropic'
);

// Use exactly like client.messages.create
const message = await slowCreate({
  model: 'claude-opus-4-5',
  max_tokens: 1024,
  messages: [
    { role: 'user', content: 'Explain the tradeoffs of eventual consistency in distributed systems.' }
  ]
});

console.log(message.content[0].text);
Wrapping a client

The fake response pattern for tests

Combine a Jest mock returning a fake Message with slowdep for realistic latency. You get deterministic, token-free test responses that still reflect production timing.

import { withLatency } from 'slowdep';
import type { Message } from '@anthropic-ai/sdk/resources';

// Build a fake Message matching the Anthropic SDK shape
function fakeMessage(text: string): Message {
  return {
    id: 'msg_test_01',
    type: 'message',
    role: 'assistant',
    model: 'claude-opus-4-5',
    stop_reason: 'end_turn',
    stop_sequence: null,
    content: [{ type: 'text', text }],
    usage: { input_tokens: 30, output_tokens: 120 }
  };
}

// Fake create: returns controlled response with realistic Claude latency
export const mockCreate = withLatency(
  jest.fn().mockResolvedValue(
    fakeMessage('Eventual consistency trades strong guarantees for availability and partition tolerance...')
  ),
  'anthropic'
);
Testing

Test timeout, retry, and streaming UX

import { withLatency } from 'slowdep';
import { generateReport } from '../src/ai/report';

test('generateReport times out after 8s and returns fallback', async () => {
  // Simulate a very slow Claude response — complex prompt with long output
  const slowCreate = withLatency(
    jest.fn().mockResolvedValue(fakeMessage('detailed report...')),
    { p50: 10000, p99: 18000 }
  );

  const result = await generateReport('Q4 data', {
    create: slowCreate,
    timeoutMs: 8000,
    fallback: 'Report generation is taking longer than expected. Please try again.'
  });

  // Verify fallback was returned instead of throwing
  expect(result).toBe('Report generation is taking longer than expected. Please try again.');
}, 12000);
import { withLatency } from 'slowdep';

test('retries on overload error (529) and succeeds', async () => {
  const overloadError = Object.assign(
    new Error('Overloaded'),
    { status: 529, error: { type: 'overloaded_error' } }
  );

  const mockCreate = jest.fn()
    .mockRejectedValueOnce(overloadError)
    .mockResolvedValueOnce(fakeMessage('success after retry'));

  const slowCreate = withLatency(mockCreate, 'anthropic');

  const result = await callWithRetry(slowCreate, {
    model: 'claude-opus-4-5',
    max_tokens: 256,
    messages: [{ role: 'user', content: 'Hello' }]
  }, { retryOn: [529], maxRetries: 2 });

  expect(result.content[0].text).toBe('success after retry');
  expect(mockCreate).toHaveBeenCalledTimes(2);
}, 20000);
import Anthropic from '@anthropic-ai/sdk';
import { withLatency } from 'slowdep';

// slowdep delays the START of the stream (time-to-first-token)
// This tests whether your UI shows a "thinking..." state before tokens arrive
test('streaming UI shows thinking state before first token', async () => {
  const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

  // Wrap the stream factory — latency applied before stream begins
  const slowStream = withLatency(
    client.messages.stream.bind(client.messages),
    'anthropic'
  );

  let thinkingStateShown = false;
  const streamPromise = slowStream({
    model: 'claude-opus-4-5',
    max_tokens: 64,
    messages: [{ role: 'user', content: 'Say hello briefly.' }]
  });

  // Immediately after kicking off stream, UI should be in "thinking" state
  thinkingStateShown = true;
  expect(thinkingStateShown).toBe(true);

  const stream = await streamPromise;
  const text = await stream.finalText();
  expect(text.length).toBeGreaterThan(0);
}, 15000);
Individual wrapping

Model-specific latency profiles

Different Claude models have different latency characteristics. claude-haiku-4-5 is significantly faster than claude-opus-4-5. Use custom profiles per model.

import Anthropic from '@anthropic-ai/sdk';
import { withLatency } from 'slowdep';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const create = client.messages.create.bind(client.messages);

// claude-haiku-4-5: very fast, great for high-throughput tasks
const slowHaiku = withLatency(create, { p50: 200, p99: 1500, errorRate: 0.003 });

// claude-sonnet-4-5: balanced speed and capability
const slowSonnet = withLatency(create, { p50: 400, p99: 4000, errorRate: 0.005 });

// claude-opus-4-5: most capable, use 'anthropic' preset (p50: 600ms, p99: 7s)
const slowOpus = withLatency(create, 'anthropic');

// Route to the right model based on task complexity
const classify = await slowHaiku({ model: 'claude-haiku-4-5', max_tokens: 16, messages: [{ role: 'user', content: 'Is this spam? Reply yes/no: "Buy now!"' }] });
const analysis = await slowOpus({ model: 'claude-opus-4-5', max_tokens: 2048, messages: [{ role: 'user', content: 'Analyze this codebase architecture...' }] });
Production safety

Never wrap real API calls

// src/ai/claude.ts — export the right client for each environment
import Anthropic from '@anthropic-ai/sdk';
import { withLatency } from 'slowdep';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// In tests: inject your mock + slowdep via dependency injection
// In production/dev: use the real client as-is
export type CreateFn = typeof client.messages.create;

export function getCreateFn(mockFn?: jest.Mock): CreateFn {
  if (mockFn) {
    // Test: use the provided mock with slowdep latency
    return withLatency(mockFn, 'anthropic') as unknown as CreateFn;
  }
  // Production/dev: real API, no artificial latency
  return client.messages.create.bind(client.messages);
}
Only use slowdep on mocks

Like all LLM integrations, only wrap a mock function with slowdep — never a real API client method. Wrapping a real API call adds fake latency on top of real latency, making your tests meaningless and your development experience painful.

What to test

Scenarios this integration helps you verify

  • Time-to-first-token UX — Claude's TTFT varies; does your UI show a "thinking" indicator within 100ms of the request starting?
  • Streaming vs buffered choice — at p50 of 600ms, buffered responses feel instant; at p99 of 7s, streaming becomes essential for UX
  • Overload handling (529) — Claude can return 529 Overloaded; your retry logic must handle it with appropriate backoff
  • Multi-turn context budget — longer conversations have higher latency; verify your UX still feels responsive after 10 turns
  • Parallel call efficiency — running two independent Claude calls in parallel cuts total latency roughly in half at p50
  • Tool use round-trips — agentic workflows make multiple sequential API calls; each call adds p50 latency — 5 tool uses = ~3s minimum
  • Fallback to cached response — when Claude is slow or unavailable, does your service return a stale cached response or error gracefully?
TypeScript

Typed Claude integration

import Anthropic from '@anthropic-ai/sdk';
import type { Message, MessageCreateParamsNonStreaming } from '@anthropic-ai/sdk/resources';
import { withLatency } from 'slowdep';

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Explicit typing for the wrapped function
const slowCreate = withLatency(
  (params: MessageCreateParamsNonStreaming): Promise<Message> =>
    client.messages.create(params) as Promise<Message>,
  'anthropic'
);

// Full type safety — result is Message, content is TextBlock[]
const response: Message = await slowCreate({
  model: 'claude-opus-4-5',
  max_tokens: 512,
  messages: [{ role: 'user', content: 'What is recursion?' }]
});

const text = response.content
  .filter(block => block.type === 'text')
  .map(block => block.text)
  .join('');