← Blog

Claude API Request Logging for Debugging: A Full Guide

2026-09-26 · 5 min read · SubToAPI Team

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:

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:

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →