Claude API Request Logging for Debugging: A Full Guide
When a Claude API integration misbehaves — a malformed tool call, an unexpected 429, a truncated stream — the fastest way to find out why is to have a complete, searchable log of every request and response. This article covers what to log, how to log it without leaking secrets or blowing up storage, and how to structure logs so you can actually debug with them instead of just staring at raw JSON.
The short answer: log the full request payload (minus the API key), the full response body or streamed chunks, HTTP status code, latency, and a correlation ID that ties a request to whatever triggered it in your app. Do this at the edge of your integration — one wrapper function around your API calls — rather than scattering console.log calls through business logic.
What to Actually Log
Not everything needs the same level of detail. For debugging purposes, capture these fields on every call:
- Request ID — generate your own UUID before the call, independent of any ID the API returns, so you can trace a request even if it fails before getting a response.
- Timestamp — start and end time, so you get latency for free.
- Model and parameters — model name,
max_tokens,temperature,systemprompt (or a hash of it if it's large/sensitive), and whetherstreamwas true. - Messages array — the full conversation sent, or at minimum the last user message and any tool results.
- Response body — full text for non-streamed calls; concatenated chunks plus the raw event types for streamed calls.
- HTTP status and error body — especially important for 4xx/5xx responses, since the error message often tells you exactly what's wrong (bad request shape, invalid tool schema, rate limit).
- Token usage — input/output token counts from the response metadata, useful for both cost debugging and spotting unexpectedly long prompts.
- Tool calls — if you're using tool use, log the tool name, input arguments, and what your code returned as the tool result, since mismatches here are a common source of bugs.
A Minimal Logging Wrapper
Here's a pattern that works regardless of which Claude client you use — wrap the call, not the caller:
async function loggedMessageCall(client, params) {
const requestId = crypto.randomUUID();
const start = Date.now();
logEvent({
type: "request",
requestId,
model: params.model,
stream: !!params.stream,
messages: params.messages,
system: params.system,
});
try {
const response = await client.messages.create(params);
logEvent({
type: "response",
requestId,
durationMs: Date.now() - start,
status: 200,
usage: response.usage,
content: response.content,
});
return response;
} catch (err) {
logEvent({
type: "error",
requestId,
durationMs: Date.now() - start,
status: err.status,
body: err.error ?? err.message,
});
throw err;
}
}
function logEvent(entry) {
console.log(JSON.stringify({ ts: new Date().toISOString(), ...entry }));
}
Emitting structured JSON lines like this means you can pipe logs into anything — a log aggregator, a local file you grep, or a database table — without changing the logging code itself.
Logging Streamed Responses
Streaming makes debugging harder because there's no single "response" object — you get a sequence of message_start, content_block_delta, and message_stop events. For debugging, buffer the deltas and log the assembled result alongside the raw event sequence:
const chunks = [];
const stream = await client.messages.stream(params);
stream.on("text", (delta) => chunks.push(delta));
stream.on("finalMessage", (message) => {
logEvent({
type: "response",
requestId,
assembledText: chunks.join(""),
usage: message.usage,
stopReason: message.stop_reason,
});
});
If you're building your own SSE parser instead of using an SDK, log every raw event type and its payload during development, then trim to just deltas and the final message once you trust the parsing logic.
Redacting Secrets and Sensitive Data
Never log your API key, and be deliberate about what user content ends up in logs, especially if messages contain PII. A simple redaction pass before logging catches most of it:
function redact(obj) {
const clone = JSON.parse(JSON.stringify(obj));
if (clone.headers?.Authorization) clone.headers.Authorization = "[redacted]";
return clone;
}
If your logs are shipped to a third-party aggregator, treat the entire payload as sensitive by default and only log what you've explicitly reviewed.
Correlating Logs with Retries and Rate Limits
Debugging gets confusing when your code retries automatically on 429s or 529s — you end up with multiple log entries for what felt like "one" request from the user's perspective. Add a parentRequestId field that stays the same across retries, and a retryAttempt counter. That way you can filter to just the final outcome, or expand to see the full retry chain when something looks off.
If You're Debugging Through SubToAPI
If your app calls Claude through SubToAPI instead of a raw Anthropic key, you get most of this logging for free at the account level — every request made with a sub_live_... key is recorded with status, latency, and token usage in the dashboard, which is useful for spotting patterns across your whole team without instrumenting every service yourself. It doesn't replace application-level logging (you still want request/response bodies in your own logs for full debugging), but it's a good cross-check when you suspect an issue is upstream rather than in your code. See the quickstart and messages docs for request/response formats, and streaming docs if you're debugging SSE issues specifically.
Retention and Storage
Full request/response logging adds up fast if you're sending long system prompts or large documents. Reasonable defaults:
- Keep full-fidelity logs (complete payloads) for 7–14 days — enough to debug recent issues.
- Downsample to metadata-only (status, latency, token counts, no bodies) beyond that for longer-term trend analysis.
- Always keep error logs longer than success logs, since they're rarer and more valuable for pattern-spotting.
Questions
Do I need to log every single API call in production? Not necessarily at full fidelity. Log metadata (status, latency, tokens) for everything, and full request/response bodies for a sampled subset or for all errors — that balances storage cost against debugging usefulness.
Should I log the system prompt on every request? If it's static, log a hash or version identifier instead of the full text each time, and store the full text once separately. This keeps logs smaller and still lets you correlate which prompt version produced which output.
How do I debug a truncated or cut-off streamed response? Log the stop_reason field from the final message alongside the assembled text — max_tokens means it hit your token limit, while other stop reasons point to a different issue like a tool call or stop sequence.