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.
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.
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);
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' );
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);
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...' }] });
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); }
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.
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?
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('');