---
title: 캐시 진단
url: https://platform.claude.com/docs/ko/build-with-claude/cache-diagnostics
description: 연속된 요청을 비교하고 프롬프트 접두사가 정확히 어디에서 달라졌는지 식별하여 예상치 못한 프롬프트 캐시 미스를 진단합니다.
featureMetadata:
  status: ga
  zdr:
    eligibility: eligible
    note: Excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements).
  supportedPlatforms:
    Claude API: ga
---

[프롬프트 캐싱](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching)은 "latency"(지연 시간)와 비용을 크게 줄여 주지만, 이는 프롬프트의 시작 부분이 최근 요청과 바이트 단위로 동일할 때에만 해당됩니다. 순서가 바뀐 도구, 시스템 프롬프트에 삽입된 타임스탬프, 또는 이전 메시지에 대한 편집은 캐시를 조용히 무효화할 수 있습니다. 캐시 진단이 없으면 유일한 신호는 `usage.cache_read_input_tokens`가 0으로 떨어지는 것뿐이며, 무엇이 변경되었는지에 대한 표시는 전혀 없습니다.

캐시 진단은 이 간극을 메워 줍니다. 이전 응답의 `id`를 전달하면 API가 두 요청을 비교하여 어디에서 달라졌는지(모델, 시스템 프롬프트, 도구 또는 메시지 기록)를 알려 주므로, 추측하는 대신 근본 원인을 수정할 수 있습니다.

## 캐시 진단의 작동 방식

`diagnostics` 객체를 포함하는 각 요청에 대해 API는 응답 `id`를 키로 하는 가벼운 "fingerprint"(지문)를 저장합니다. 이 객체를 생략한 요청에 대해서는 아무것도 저장하지 않습니다. 다음 요청에서 이전 응답의 `id`를 `diagnostics.previous_message_id`로 포함하세요. API는 새 요청에 대한 지문을 다시 생성하고, 저장된 지문과 비교한 다음, 첫 번째 분기 지점을 설명하는 `diagnostics` 객체를 응답에 첨부합니다.

이 비교는 캐시가 실제로 히트했는지 여부와 무관하게 요청 구조에 관한 것입니다. `diagnostics` 결과를 `usage.cache_read_input_tokens`와 결합하는 방법은 [usage와 함께 진단 읽기](https://platform.claude.com/docs/ko/build-with-claude/cache-diagnostics#reading-diagnostics-alongside-usage)를 참조하세요.

지문에는 해시와 토큰 수 추정치만 포함되며(원시 프롬프트 내용은 절대 포함되지 않음), 제한된 시간 동안만 보존되고, 조직 및 워크스페이스 범위로 한정되며, 다른 어떤 목적으로도 사용되지 않습니다.

## 기본 사용법

모든 턴에 `diagnostics` 객체를 포함하세요. 이 객체가 옵트인 역할을 합니다. API는 이 객체를 포함한 요청에 대해서만 지문을 저장합니다. 첫 번째 턴에서는 비교할 이전 메시지 없이 옵트인하기 위해 `"previous_message_id": null`을 전달하세요. 이후 턴에서는 이전 응답의 `id`를 전달하세요. `cache-diagnosis-2026-04-07` 베타 헤더는 더 이상 필요하지 않으며, 여전히 이 헤더를 보내는 요청도 이전과 같이 작동합니다.

<CodeGroup>
  ```bash cURL
  # 1턴: 캐시를 생성하고 진단 기능을 활성화합니다
  response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "cache_control": {"type": "ephemeral"},
      "system": "You are an AI assistant analyzing a large document. <document>...</document>",
      "messages": [{"role": "user", "content": "Summarize section 1."}],
      "diagnostics": {"previous_message_id": null}
    }')
  jq '{id, diagnostics}' <<< "$response"
  message_id=$(jq -r '.id' <<< "$response")

  # 2턴: API가 접두사를 비교할 수 있도록 이전 턴을 참조합니다
  curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "cache_control": {"type": "ephemeral"},
      "system": "You are an AI assistant analyzing a large document. <document>...</document>",
      "messages": [
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": "Section 1 covers..."},
        {"role": "user", "content": "Now summarize section 2."}
      ],
      "diagnostics": {"previous_message_id": "'"$message_id"'"}
    }' | jq '{id, diagnostics}'  # diagnostics: null means no divergence was found
  ```

  ```bash CLI
  # 턴 1
  turn1=$(ant beta:messages create \
    --transform '{id,usage,diagnostics}' <<'YAML'
  model: claude-opus-5-5
  max_tokens: 1024
  cache_control:
    type: ephemeral
  system: "You are an AI assistant analyzing a large document. <document>...</document>"
  messages:
    - role: user
      content: Summarize section 1.
  diagnostics:
    previous_message_id: null
  YAML
  )
  printf '%s\n' "$turn1"

  # 턴 2: 턴 1의 id를 previous_message_id로 전달합니다
  message_id=$(jq -r '.id' <<<"$turn1")
  ant beta:messages create \
    --transform '{id,usage,diagnostics}' <<YAML
  model: claude-opus-5-5
  max_tokens: 1024
  cache_control:
    type: ephemeral
  system: "You are an AI assistant analyzing a large document. <document>...</document>"
  messages:
    - role: user
      content: Summarize section 1.
    - role: assistant
      content: Section 1 covers...
    - role: user
      content: Now summarize section 2.
  diagnostics:
    previous_message_id: $message_id
  YAML
  ```

  ```python Python
  client = anthropic.Anthropic()

  SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

  # 턴 1: previous_message_id=None으로 옵트인합니다
  r1 = client.beta.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      cache_control={"type": "ephemeral"},
      system=SYSTEM,
      messages=[{"role": "user", "content": "Summarize section 1."}],
      diagnostics={"previous_message_id": None},
  )

  # 턴 2: 이전 응답 id를 참조합니다
  r2 = client.beta.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      cache_control={"type": "ephemeral"},
      system=SYSTEM,
      messages=[
          {"role": "user", "content": "Summarize section 1."},
          {"role": "assistant", "content": r1.content},
          {"role": "user", "content": "Now summarize section 2."},
      ],
      diagnostics={"previous_message_id": r1.id},
  )

  diagnostics = r2.diagnostics
  if diagnostics is None:
      print("No divergence detected.")
  elif diagnostics.cache_miss_reason is None:
      print("Comparison still pending.")
  else:
      print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}")
  ```

  ```typescript TypeScript
  const client = new Anthropic();

  const SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>";

  // 턴 1: previous_message_id: null로 옵트인합니다
  const r1 = await client.beta.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    cache_control: { type: "ephemeral" },
    system: SYSTEM,
    messages: [{ role: "user", content: "Summarize section 1." }],
    diagnostics: { previous_message_id: null }
  });

  // 턴 2: 이전 응답 id를 참조합니다
  const r2 = await client.beta.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    cache_control: { type: "ephemeral" },
    system: SYSTEM,
    messages: [
      { role: "user", content: "Summarize section 1." },
      { role: "assistant", content: r1.content },
      { role: "user", content: "Now summarize section 2." }
    ],
    diagnostics: { previous_message_id: r1.id }
  });

  if (r2.diagnostics === null) {
    console.log("No divergence detected.");
  } else if (r2.diagnostics.cache_miss_reason === null) {
    console.log("Comparison still pending.");
  } else {
    console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`);
  }
  ```

  ```csharp C#
  AnthropicClient client = new();

  var system = "You are an AI assistant analyzing a large document. <document>...</document>";

  var r1 = await client.Beta.Messages.Create(
      new()
      {
          Model = Messages::Model.ClaudeOpus5_5,
          MaxTokens = 1024,
          CacheControl = new(),
          System = system,
          Messages =
          [
              new() { Role = Role.User, Content = "Summarize section 1." },
          ],
          Diagnostics = new() { PreviousMessageID = null },
      }
  );

  var r2 = await client.Beta.Messages.Create(
      new()
      {
          Model = Messages::Model.ClaudeOpus5_5,
          MaxTokens = 1024,
          CacheControl = new(),
          System = system,
          Messages =
          [
              new() { Role = Role.User, Content = "Summarize section 1." },
              new()
              {
                  Role = Role.Assistant,
                  Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
              },
              new() { Role = Role.User, Content = "Now summarize section 2." },
          ],
          Diagnostics = new() { PreviousMessageID = r1.ID },
      }
  );

  Console.WriteLine(r2.Diagnostics switch
  {
      null => "No divergence detected.",
      { CacheMissReason: null } => "Comparison still pending.",
      { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}",
  });
  ```

  ```go Go
  client := anthropic.NewClient()
  ctx := context.Background()

  system := []anthropic.BetaTextBlockParam{
  	{Text: "You are an AI assistant analyzing a large document. <document>...</document>"},
  }

  r1, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
  	Model:        anthropic.ModelClaudeOpus5_5,
  	MaxTokens:    1024,
  	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
  	System:       system,
  	Messages: []anthropic.BetaMessageParam{
  		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
  	},
  	Diagnostics: anthropic.BetaDiagnosticsParam{
  		PreviousMessageID: param.Null[string](),
  	},
  })
  if err != nil {
  	panic(err)
  }

  r2, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
  	Model:        anthropic.ModelClaudeOpus5_5,
  	MaxTokens:    1024,
  	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
  	System:       system,
  	Messages: []anthropic.BetaMessageParam{
  		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
  		r1.ToParam(),
  		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")),
  	},
  	Diagnostics: anthropic.BetaDiagnosticsParam{
  		PreviousMessageID: anthropic.String(r1.ID),
  	},
  })
  if err != nil {
  	panic(err)
  }

  switch {
  case !r2.JSON.Diagnostics.Valid():
  	fmt.Println("No divergence detected.")
  case !r2.Diagnostics.JSON.CacheMissReason.Valid():
  	fmt.Println("Comparison still pending.")
  default:
  	fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type)
  }
  ```

  ```java Java
  var client = AnthropicOkHttpClient.fromEnv();

  var system = "You are an AI assistant analyzing a large document. <document>...</document>";

  var r1 = client.beta().messages().create(
      MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .cacheControl(BetaCacheControlEphemeral.builder().build())
          .system(system)
          .addUserMessage("Summarize section 1.")
          // 첫 번째 턴에서는 비교할 이전 메시지 없이 옵트인하려면 null을 전달하세요.
          .diagnostics(BetaDiagnosticsParam.builder().previousMessageId((String) null).build())
          .build()
  );

  var r2 = client.beta().messages().create(
      MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .cacheControl(BetaCacheControlEphemeral.builder().build())
          .system(system)
          .addUserMessage("Summarize section 1.")
          .addMessage(r1)
          .addUserMessage("Now summarize section 2.")
          .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
          .build()
  );

  if (r2.diagnostics().isEmpty()) {
      IO.println("No divergence detected.");
  } else if (r2.diagnostics().get().cacheMissReason().isEmpty()) {
      IO.println("Comparison still pending.");
  } else {
      var reason = r2.diagnostics().get().cacheMissReason().get();
      // CacheMissReason은 타입이 지정된 .type() 접근자를 제공하지 않으므로 원시 JSON에서 읽으세요.
      @SuppressWarnings("unchecked")
      var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
      IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow());
  }
  ```

  ```php PHP
  $client = new Client();

  $system = 'You are an AI assistant analyzing a large document. <document>...</document>';

  $r1 = $client->beta->messages->create(
      model: Model::CLAUDE_OPUS_5_5,
      maxTokens: 1024,
      cacheControl: new BetaCacheControlEphemeral,
      system: $system,
      messages: [
          ['role' => 'user', 'content' => 'Summarize section 1.'],
      ],
      diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID(null),
  );

  $r2 = $client->beta->messages->create(
      model: Model::CLAUDE_OPUS_5_5,
      maxTokens: 1024,
      cacheControl: new BetaCacheControlEphemeral,
      system: $system,
      messages: [
          ['role' => 'user', 'content' => 'Summarize section 1.'],
          ['role' => 'assistant', 'content' => $r1->content],
          ['role' => 'user', 'content' => 'Now summarize section 2.'],
      ],
      diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
  );

  echo match (true) {
      $r2->diagnostics === null => "No divergence detected.\n",
      $r2->diagnostics->cacheMissReason === null => "Comparison still pending.\n",
      default => "cache_miss_reason: {$r2->diagnostics->cacheMissReason->type}\n",
  };
  ```

  ```ruby Ruby
  client = Anthropic::Client.new

  SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

  r1 = client.beta.messages.create(
    model: :"claude-opus-5-5",
    max_tokens: 1024,
    cache_control: {type: "ephemeral"},
    system_: SYSTEM,
    messages: [
      {role: "user", content: "Summarize section 1."}
    ],
    diagnostics: {previous_message_id: nil}
  )

  r2 = client.beta.messages.create(
    model: :"claude-opus-5-5",
    max_tokens: 1024,
    cache_control: {type: "ephemeral"},
    system_: SYSTEM,
    messages: [
      {role: "user", content: "Summarize section 1."},
      {role: "assistant", content: r1.content},
      {role: "user", content: "Now summarize section 2."}
    ],
    diagnostics: {previous_message_id: r1.id}
  )

  case r2.diagnostics
  in nil
    puts "No divergence detected."
  in {cache_miss_reason: nil}
    puts "Comparison still pending."
  in {cache_miss_reason: {type:}}
    puts "cache_miss_reason: #{type}"
  end
  ```
</CodeGroup>

## 스트리밍

스트리밍 응답에서 `diagnostics`는 `message_start` 이벤트에 나타납니다.

<CodeGroup>
  ```bash cURL
  # 턴 2: 응답을 스트리밍합니다. diagnostics는 message_start 이벤트에 포함되어 전달됩니다.
  # 값이 null이면 불일치가 발견되지 않았음을 의미합니다.
  curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "stream": true,
      "cache_control": {"type": "ephemeral"},
      "system": "You are an AI assistant analyzing a large document. <document>...</document>",
      "messages": [
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": "Section 1 covers..."},
        {"role": "user", "content": "Now summarize section 2."}
      ],
      "diagnostics": {"previous_message_id": "'"$message_id"'"}
    }' | jq -R 'select(startswith("data: ")) | ltrimstr("data: ") | fromjson | select(.type == "message_start") | .message.diagnostics'
  ```

  ```bash CLI
  # 턴 2: 스트리밍. --stream을 사용하면 CLI가 각 SSE 이벤트를 하나의 JSON 객체로 출력합니다.
  # diagnostics는 message_start 이벤트에 포함되어 전달되므로 jq로 추출하세요.
  ant beta:messages create \
    --stream --format jsonl <<YAML |
  model: claude-opus-5-5
  max_tokens: 1024
  cache_control:
    type: ephemeral
  system: "You are an AI assistant analyzing a large document. <document>...</document>"
  messages:
    - role: user
      content: Summarize section 1.
    - role: assistant
      content: Section 1 covers...
    - role: user
      content: Now summarize section 2.
  diagnostics:
    previous_message_id: $message_id
  YAML
    jq -c 'select(.type == "message_start") | .message | {id,usage,diagnostics}'
  ```

  ```python Python
  # 턴 2: 이전 응답 ID를 참조하여 스트리밍합니다
  with client.beta.messages.stream(
      model="claude-opus-5-5",
      max_tokens=1024,
      cache_control={"type": "ephemeral"},
      system=SYSTEM,
      messages=[
          {"role": "user", "content": "Summarize section 1."},
          {"role": "assistant", "content": r1.content},
          {"role": "user", "content": "Now summarize section 2."},
      ],
      diagnostics={"previous_message_id": r1.id},
  ) as stream:
      for text in stream.text_stream:
          print(text, end="", flush=True)
      print()
      r2 = stream.get_final_message()

  diagnostics = r2.diagnostics
  if diagnostics is None:
      print("No divergence detected.")
  elif diagnostics.cache_miss_reason is None:
      print("Comparison still pending.")
  else:
      print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}")
  ```

  ```typescript TypeScript
  const stream = client.beta.messages.stream({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    cache_control: { type: "ephemeral" },
    system: SYSTEM,
    messages: [
      { role: "user", content: "Summarize section 1." },
      { role: "assistant", content: r1.content },
      { role: "user", content: "Now summarize section 2." }
    ],
    diagnostics: { previous_message_id: r1.id }
  });

  for await (const event of stream) {
    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      process.stdout.write(event.delta.text);
    }
  }
  process.stdout.write("\n");

  // diagnostics는 message_start에서 도착하며 최종 메시지까지 전달됩니다
  const r2 = await stream.finalMessage();

  if (r2.diagnostics === null) {
    console.log("No divergence detected.");
  } else if (r2.diagnostics.cache_miss_reason === null) {
    console.log("Comparison still pending.");
  } else {
    console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`);
  }
  ```

  ```csharp C#
  // 턴 2: 이전 응답 id를 참조하여 스트리밍합니다
  BetaDiagnostics? diagnostics = null;

  var stream = client.Beta.Messages.CreateStreaming(
      new()
      {
          Model = Messages::Model.ClaudeOpus5_5,
          MaxTokens = 1024,
          CacheControl = new(),
          System = system,
          Messages =
          [
              new() { Role = Role.User, Content = "Summarize section 1." },
              new()
              {
                  Role = Role.Assistant,
                  Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
              },
              new() { Role = Role.User, Content = "Now summarize section 2." },
          ],
          Diagnostics = new() { PreviousMessageID = r1.ID },
      }
  );

  await foreach (var streamEvent in stream)
  {
      if (streamEvent.TryPickStart(out var start))
      {
          // diagnostics는 message_start 이벤트에서 전달됩니다
          diagnostics = start.Message.Diagnostics;
      }
      else if (streamEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickText(out var textDelta))
      {
          Console.Write(textDelta.Text);
      }
  }
  Console.WriteLine();

  Console.WriteLine(diagnostics switch
  {
      null => "No divergence detected.",
      { CacheMissReason: null } => "Comparison still pending.",
      { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}",
  });
  ```

  ```go Go
  // 턴 2: 이전 응답 id를 참조하여 스트리밍합니다
  stream := client.Beta.Messages.NewStreaming(ctx, anthropic.BetaMessageNewParams{
  	Model:        anthropic.ModelClaudeOpus5_5,
  	MaxTokens:    1024,
  	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
  	System:       system,
  	Messages: []anthropic.BetaMessageParam{
  		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
  		r1.ToParam(),
  		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")),
  	},
  	Diagnostics: anthropic.BetaDiagnosticsParam{
  		PreviousMessageID: anthropic.String(r1.ID),
  	},
  })
  defer stream.Close()

  // diagnostics는 message_start에서 전달되며, Accumulate가 이를 r2로 옮깁니다
  var r2 anthropic.BetaMessage
  for stream.Next() {
  	if err := r2.Accumulate(stream.Current()); err != nil {
  		panic(err)
  	}
  }
  if err := stream.Err(); err != nil {
  	panic(err)
  }

  switch {
  case !r2.JSON.Diagnostics.Valid():
  	fmt.Println("No divergence detected.")
  case !r2.Diagnostics.JSON.CacheMissReason.Valid():
  	fmt.Println("Comparison still pending.")
  default:
  	fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type)
  }
  ```

  ```java Java
  // 턴 2: 이전 응답 id를 참조하여 스트리밍합니다
  var params = MessageCreateParams.builder()
      .model(Model.CLAUDE_OPUS_5_5)
      .maxTokens(1024)
      .cacheControl(BetaCacheControlEphemeral.builder().build())
      .system(system)
      .addUserMessage("Summarize section 1.")
      .addMessage(r1)
      .addUserMessage("Now summarize section 2.")
      .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
      .build();

  var accumulator = BetaMessageAccumulator.create();
  try (var streamResponse = client.beta().messages().createStreaming(params)) {
      streamResponse.stream()
          .peek(accumulator::accumulate)
          .flatMap(event -> event.contentBlockDelta().stream())
          .flatMap(deltaEvent -> deltaEvent.delta().text().stream())
          .forEach(textDelta -> IO.print(textDelta.text()));
      IO.println("");
  }

  // diagnostics는 message_start에서 도착하며 누적된 메시지까지 그대로 전달됩니다
  var diagnostics = accumulator.message().diagnostics();
  if (diagnostics.isEmpty()) {
      IO.println("No divergence detected.");
  } else if (diagnostics.get().cacheMissReason().isEmpty()) {
      IO.println("Comparison still pending.");
  } else {
      var reason = diagnostics.get().cacheMissReason().get();
      // CacheMissReason은 타입이 지정된 .type() 접근자를 제공하지 않으므로 원시 JSON에서 읽습니다.
      @SuppressWarnings("unchecked")
      var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
      IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow());
  }
  ```

  ```php PHP
  // 턴 2: 이전 응답 id를 참조하여 스트리밍합니다
  $stream = $client->beta->messages->createStream(
      model: Model::CLAUDE_OPUS_5_5,
      maxTokens: 1024,
      cacheControl: new BetaCacheControlEphemeral,
      system: $system,
      messages: [
          ['role' => 'user', 'content' => 'Summarize section 1.'],
          ['role' => 'assistant', 'content' => $r1->content],
          ['role' => 'user', 'content' => 'Now summarize section 2.'],
      ],
      diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
  );

  $diagnostics = null;
  foreach ($stream as $event) {
      switch (true) {
          case $event instanceof \Anthropic\Beta\Messages\BetaRawMessageStartEvent:
              // diagnostics는 message_start 이벤트에 포함된 BetaMessage로 전달됩니다
              $diagnostics = $event->message->diagnostics;
              break;
          case $event instanceof \Anthropic\Beta\Messages\BetaRawContentBlockDeltaEvent:
              if ($event->delta instanceof \Anthropic\Beta\Messages\BetaTextDelta) {
                  echo $event->delta->text;
              }
              break;
      }
  }
  echo PHP_EOL;

  echo match (true) {
      $diagnostics === null => "No divergence detected.\n",
      $diagnostics->cacheMissReason === null => "Comparison still pending.\n",
      default => "cache_miss_reason: {$diagnostics->cacheMissReason->type}\n",
  };
  ```

  ```ruby Ruby
  # 턴 2: 이전 응답 id를 참조하여 스트리밍합니다
  stream = client.beta.messages.stream(
    model: :"claude-opus-5-5",
    max_tokens: 1024,
    cache_control: {type: "ephemeral"},
    system_: SYSTEM,
    messages: [
      {role: "user", content: "Summarize section 1."},
      {role: "assistant", content: r1.content},
      {role: "user", content: "Now summarize section 2."}
    ],
    diagnostics: {previous_message_id: r1.id}
  )

  stream.each do |event|
    print(event.text) if event.is_a?(Anthropic::Streaming::TextEvent)
  end
  puts

  # diagnostics는 message_start에서 전달되며 누적된 메시지에 유지됩니다
  r2 = stream.accumulated_message

  case r2.diagnostics
  in nil
    puts "No divergence detected."
  in {cache_miss_reason: nil}
    puts "Comparison still pending."
  in {cache_miss_reason: {type:}}
    puts "cache_miss_reason: #{type}"
  end
  ```
</CodeGroup>

`message_start` 이벤트는 전체 `diagnostics` 필드를 담고 있습니다. 가능한 값은 [응답 형식](https://platform.claude.com/docs/ko/build-with-claude/cache-diagnostics#response-format)을 참조하세요.

## 대화 루프 전반에 걸쳐 진단 연결하기

멀티턴 대화에서는 매 턴마다 최신 응답 `id`를 `previous_message_id`로 전달하세요. 첫 번째 반복에서는 옵트인을 위해 `null`을 전달하고, 이후 각 반복에서는 이전 응답의 `id`를 전달합니다.

<Tabs>
  <Tab title="cURL">
    <Info>
      이 워크플로는 일회성 셸 명령으로 잘 변환되지 않습니다. 루프 패턴은 SDK 탭을 참조하세요. 턴별 HTTP 요청은 [기본 사용법](https://platform.claude.com/docs/ko/build-with-claude/cache-diagnostics#basic-usage)과 동일합니다.
    </Info>
  </Tab>

  <Tab title="CLI">
    <Info>
      이 워크플로는 일회성 셸 명령으로 잘 변환되지 않습니다. 루프 패턴은 SDK 탭을 참조하세요. 턴별 CLI 호출은 [기본 사용법](https://platform.claude.com/docs/ko/build-with-claude/cache-diagnostics#basic-usage)과 동일합니다.
    </Info>
  </Tab>

  <Tab title="Python">
    ```python
    client = anthropic.Anthropic()

    SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

    messages = []
    prev_id = None

    for i, user_message in enumerate(
        ["Summarize section 1.", "Now section 2.", "Now section 3."]
    ):
        messages.append({"role": "user", "content": user_message})

        r = client.beta.messages.create(
            model="claude-opus-5-5",
            max_tokens=1024,
            cache_control={"type": "ephemeral"},
            system=SYSTEM,
            messages=messages,
            diagnostics={"previous_message_id": prev_id},
        )

        if r.diagnostics is not None and r.diagnostics.cache_miss_reason is not None:
            print(f"Turn {i + 1} cache_miss_reason: {r.diagnostics.cache_miss_reason.type}")

        messages.append({"role": "assistant", "content": r.content})
        prev_id = r.id
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript
    const client = new Anthropic();

    const SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>";

    const prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."];

    const messages: BetaMessageParam[] = [];
    let prevId: string | null = null;

    for (const [i, prompt] of prompts.entries()) {
      messages.push({ role: "user", content: prompt });

      const r: BetaMessage = await client.beta.messages.create({
        model: "claude-opus-5-5",
        max_tokens: 1024,
        cache_control: { type: "ephemeral" },
        system: SYSTEM,
        messages,
        diagnostics: { previous_message_id: prevId }
      });

      if (r.diagnostics?.cache_miss_reason) {
        console.log(`Turn ${i + 1} cache_miss_reason: ${r.diagnostics.cache_miss_reason.type}`);
      }

      messages.push({ role: "assistant", content: r.content });
      prevId = r.id;
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp
    AnthropicClient client = new();

    var system = "You are an AI assistant analyzing a large document. <document>...</document>";

    List<BetaMessageParam> messages = [];
    string? prevId = null;
    string[] prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."];

    for (int i = 0; i < prompts.Length; i++)
    {
        messages.Add(new() { Role = Role.User, Content = prompts[i] });

        var r = await client.Beta.Messages.Create(
            new()
            {
                Model = Messages::Model.ClaudeOpus5_5,
                MaxTokens = 1024,
                CacheControl = new(),
                System = system,
                Messages = messages,
                Diagnostics = new() { PreviousMessageID = prevId },
            }
        );

        if (r.Diagnostics?.CacheMissReason is { Type: var type })
        {
            Console.WriteLine($"Turn {i + 1} cache_miss_reason: {type.GetString()}");
        }

        messages.Add(
            new()
            {
                Role = Role.Assistant,
                Content = r.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
            }
        );
        prevId = r.ID;
    }
    ```
  </Tab>

  <Tab title="Go">
    ```go
    client := anthropic.NewClient()
    ctx := context.Background()

    system := []anthropic.BetaTextBlockParam{
    	{Text: "You are an AI assistant analyzing a large document. <document>...</document>"},
    }

    prompts := []string{"Summarize section 1.", "Now section 2.", "Now section 3."}

    var messages []anthropic.BetaMessageParam
    prevID := param.Null[string]()

    for turn, prompt := range prompts {
    	messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(prompt)))

    	r, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
    		Model:        anthropic.ModelClaudeOpus5_5,
    		MaxTokens:    1024,
    		CacheControl: anthropic.BetaCacheControlEphemeralParam{},
    		System:       system,
    		Messages:     messages,
    		Diagnostics: anthropic.BetaDiagnosticsParam{
    			PreviousMessageID: prevID,
    		},
    	})
    	if err != nil {
    		panic(err)
    	}

    	if r.JSON.Diagnostics.Valid() && r.Diagnostics.JSON.CacheMissReason.Valid() {
    		fmt.Printf("Turn %d cache_miss_reason: %s\n", turn+1, r.Diagnostics.CacheMissReason.Type)
    	}

    	messages = append(messages, r.ToParam())
    	prevID = anthropic.String(r.ID)
    }
    ```
  </Tab>

  <Tab title="Java">
    ```java
    var client = AnthropicOkHttpClient.fromEnv();

    var system = "You are an AI assistant analyzing a large document. <document>...</document>";
    var prompts = List.of("Summarize section 1.", "Now section 2.", "Now section 3.");

    var messages = new ArrayList<BetaMessageParam>();
    String prevId = null;

    for (var turn = 0; turn < prompts.size(); turn++) {
        messages.add(
            BetaMessageParam.builder()
                .role(BetaMessageParam.Role.USER)
                .content(prompts.get(turn))
                .build()
        );

        var r = client.beta().messages().create(
            MessageCreateParams.builder()
                .model(Model.CLAUDE_OPUS_5_5)
                .maxTokens(1024)
                .cacheControl(BetaCacheControlEphemeral.builder().build())
                .system(system)
                .messages(messages)
                .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(prevId).build())
                .build()
        );

        if (r.diagnostics().isPresent() && r.diagnostics().get().cacheMissReason().isPresent()) {
            var reason = r.diagnostics().get().cacheMissReason().get();
            // CacheMissReason은 타입이 지정된 .type() 접근자를 제공하지 않으므로 원시 JSON에서 읽습니다.
            @SuppressWarnings("unchecked")
            var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
            IO.println("Turn " + (turn + 1) + " cache_miss_reason: " + json.get("type").asStringOrThrow());
        }

        messages.add(r.toParam());
        prevId = r.id();
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php
    $client = new Client();

    $system = 'You are an AI assistant analyzing a large document. <document>...</document>';

    $messages = [];
    $prevId = null;

    foreach (['Summarize section 1.', 'Now section 2.', 'Now section 3.'] as $i => $userMsg) {
        $turn = $i + 1;
        $messages[] = ['role' => 'user', 'content' => $userMsg];

        $r = $client->beta->messages->create(
            model: Model::CLAUDE_OPUS_5_5,
            maxTokens: 1024,
            cacheControl: new BetaCacheControlEphemeral,
            system: $system,
            messages: $messages,
            diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($prevId),
        );

        if ($r->diagnostics?->cacheMissReason !== null) {
            echo "Turn {$turn} cache_miss_reason: {$r->diagnostics->cacheMissReason->type}\n";
        }

        $messages[] = ['role' => 'assistant', 'content' => $r->content];
        $prevId = $r->id;
    }
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby
    client = Anthropic::Client.new

    SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

    messages = []
    prev_id = nil

    ["Summarize section 1.", "Now section 2.", "Now section 3."].each_with_index do |user_msg, i|
      messages << {role: "user", content: user_msg}

      r = client.beta.messages.create(
        model: :"claude-opus-5-5",
        max_tokens: 1024,
        cache_control: {type: "ephemeral"},
        system_: SYSTEM,
        messages: messages,
        diagnostics: {previous_message_id: prev_id}
      )

      if (reason = r.diagnostics&.cache_miss_reason)
        puts "Turn #{i + 1} cache_miss_reason: #{reason.type}"
      end

      messages << {role: "assistant", content: r.content}
      prev_id = r.id
    end
    ```
  </Tab>
</Tabs>

## 응답 형식

응답 `Message`의 `diagnostics` 필드는 세 가지 값을 가질 수 있습니다:

| 값                              | 의미                                                                                                                                 |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `null`                         | 요청에 `diagnostics` 객체가 포함되지 않았거나, `previous_message_id`가 `null`이었거나(첫 번째 턴으로 비교할 대상이 없음), 비교가 실행되었지만 차이가 발견되지 않았습니다.                |
| `{"cache_miss_reason": null}`  | 응답이 직렬화될 때 비교가 아직 실행 중이었습니다. 이는 응답이 매우 빠르게 시작될 때 발생할 수 있습니다. 결론이 나지 않은 것으로 간주하고 다음 턴을 확인하세요.                                       |
| `{"cache_miss_reason": {...}}` | `cache_miss_reason`이 첨부되었습니다. `*_changed` 유형의 경우 첫 번째 분기 지점을 식별하며, `previous_message_not_found`와 `unavailable`은 비교가 생성되지 않은 경우입니다. |

`cache_miss_reason`이 null이 아닌 경우 다음과 같은 형태입니다:

```json
{
  "id": "msg_01Xyz...",
  "type": "message",
  "role": "assistant",
  "content": [{ "type": "text", "text": "..." }],
  "usage": {
    "input_tokens": 42,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 41850,
    "output_tokens": 210
  },
  "diagnostics": {
    "cache_miss_reason": {
      "type": "system_changed",
      "cache_missed_input_tokens": 41850
    }
  }
}
```

## 캐시 미스 사유 유형

`cache_miss_reason`은 `type`을 기준으로 하는 판별 유니온(discriminated union)입니다. 응답은 가장 이른 분기만 보고하므로 이를 먼저 수정하세요. 이후의 분기는 그 뒤에 숨겨져 있을 수 있습니다.

| 유형                           | 의미                                                                                                                                                                                                                                                                     | 변경해야 할 사항                                                                                                                                                                                                     |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model_changed`              | `model`이 이전 요청과 다릅니다(예: 라우터, A/B 테스트 또는 폴백이 다른 모델을 선택함). 캐시는 모델별로 유지됩니다.                                                                                                                                                                                               | 캐시된 대화 내에서는 모델을 일정하게 유지하세요.                                                                                                                                                                                   |
| `system_changed`             | `system` 매개변수가 다릅니다. 일반적으로 타임스탬프, 요청 ID 또는 기타 요청별 값이 시스템 프롬프트에 삽입된 경우입니다.                                                                                                                                                                                              | 시스템 프롬프트를 바이트 단위로 안정적인 상수로 만들고, 동적 데이터는 캐시 중단점 이후의 첫 번째 `user` 메시지로 옮기세요.                                                                                                                                     |
| `tools_changed`              | `tools` 배열이 다릅니다. 턴 사이에 도구가 추가, 제거 또는 재정렬되었거나, 도구 `input_schema` JSON이 비결정적으로 직렬화되었습니다.                                                                                                                                                                                | 매 턴마다 동일한 도구 목록을 고정된 순서로, 결정적으로 직렬화된 스키마(예: 키 정렬)와 함께 전송하세요.                                                                                                                                                  |
| `messages_changed`           | 모델, 시스템, 도구는 모두 일치하지만 `messages`의 이전 항목이 추가(append)되는 대신 변경, 재정렬 또는 제거되었습니다. 일반적으로 대화 기록이 잘리거나 편집되었거나, 어시스턴트 턴과 `tool_result` 블록이 재전송 시 다르게 재직렬화된 경우입니다.                                                                                                               | 기록을 추가 전용(append-only)으로 취급하고, 어시스턴트 `content`와 도구 결과를 그대로 되돌려 보내세요.                                                                                                                                          |
| `previous_message_not_found` | 제공된 `previous_message_id`에 대해 저장된 지문이 없습니다. 이는 요청이 변경되었다는 증거가 아닙니다. 일반적으로 이전 요청에 `diagnostics` 객체가 포함되지 않았거나, 다른 워크스페이스에서 온 요청이거나, 전송된 후 너무 많은 시간이 지난 경우입니다.                                                                                                           | 매 턴마다 `diagnostics` 객체를 포함하고, 연속된 턴 사이의 시간 간격을 짧게 유지하세요.                                                                                                                                                      |
| `unavailable`                | 이 요청에 대한 진단 정보를 사용할 수 없었습니다. 여기에는 `model`, `system`, `tools`는 일치하지만 프롬프트에 영향을 주는 다른 요청 매개변수(`tool_choice`, `thinking`, `context_management`, `output_config`, `output_format` 또는 활성 `anthropic-beta` 헤더 집합)가 다른 경우와, 분기가 비교 범위를 벗어난 매우 긴 대화가 포함됩니다. 요청은 정상적으로 처리되었습니다. | 캐시된 대화의 수명 동안 프롬프트에 영향을 주는 요청 매개변수를 일정하게 유지하세요. 지속적으로 발생하는 경우 프롬프트 캐싱 페이지의 [일반적인 문제 해결](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching#troubleshooting-common-issues)에 있는 수동 점검을 적용하세요. |

<Note>
  네 가지 `*_changed` 유형은 `cache_missed_input_tokens` 정수도 함께 제공합니다. 이는 분기 지점 이후에 해당하는 입력 토큰 수의 추정치로, 캐시 가능한 접두사가 얼마나 손실되었는지 감을 잡을 수 있게 해 줍니다. 토큰화 이전의 바이트 길이에서 도출되므로 청구 수치가 아닌 규모 지표로 취급하세요. `usage.input_tokens`와 다를 수 있으며(때로는 초과할 수도 있습니다).
</Note>

## usage와 함께 진단 읽기

`diagnostics`는 "내 요청이 변경되었는가?"에 답하고, `usage.cache_read_input_tokens`는 "캐시가 히트했는가?"에 답합니다. 이 둘을 결합하면 어디를 살펴봐야 할지 알 수 있습니다.

이 매트릭스는 실제 `previous_message_id`를 전달한 턴에 적용됩니다. 첫 번째 턴(`previous_message_id: null`)에서는 `diagnostics`가 항상 `null`이며, 캐시가 읽히는 것이 아니라 기록되는 중이므로 `cache_read_input_tokens`는 보통 0입니다. 문제 해결이 필요하지 않습니다. 또한 이 매트릭스는 `cache_miss_reason`이 `null`인 경우(비교가 아직 보류 중이므로 다음 턴을 확인하세요)나 그 `type`이 `previous_message_not_found` 또는 `unavailable`인 경우(비교가 생성되지 않음)에는 적용되지 않습니다.

| 진단 결과                               | 캐시 읽기 토큰 | 해석                                                                                                                                                                    |
| ----------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `null`                              | 높음       | 예상대로 작동 중입니다. 접두사가 안정적이며 캐시가 히트했습니다.                                                                                                                                  |
| `null`                              | 낮음 또는 0  | 요청은 일치하지만 캐시 항목을 더 이상 사용할 수 없었습니다. 턴 사이의 간격을 줄이거나 [1시간 캐시 TTL](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching#1-hour-cache-duration) 사용을 고려하세요. |
| `cache_miss_reason`이 `*_changed` 유형 | 낮음 또는 0  | 사용자 측 버그입니다. 요청이 변경되었으므로 `type`이 가리키는 원인을 수정하세요.                                                                                                                      |
| `cache_miss_reason`이 `*_changed` 유형 | 높음       | 드문 경우입니다. 프롬프트 후반부에서 변경이 발생했지만 앞쪽의 `cache_control` 중단점은 여전히 히트했습니다. 수정할 가치는 있지만 영향은 적습니다.                                                                             |

## 제한 사항

* **Claude API 전용:** Amazon Bedrock 또는 Google Cloud에서는 사용할 수 없습니다.
* **제한된 보존 기간:** `previous_message_id` 조회를 위한 지문은 짧은 기간 후 만료됩니다. 시간 간격이 가까운 요청 간에 진단 비교를 실행하세요.
* **동일 워크스페이스:** 이전 요청은 동일한 조직 및 워크스페이스에서 실행되었어야 합니다. 확인하려면 두 응답의 `anthropic-workspace-id` [응답 헤더](https://platform.claude.com/docs/ko/api/overview#response-headers)를 비교하세요.
* **비교 범위:** 유일한 변경 사항이 메시지 목록 깊숙한 곳에 있는 매우 긴 대화의 경우, 응답이 정확한 위치 대신 `unavailable`일 수 있습니다.
* **최선 노력(best-effort):** 진단은 요청을 차단하거나 실패시키지 않습니다. 진단 정보를 사용할 수 없는 경우 응답은 `unavailable`을 반환하며, 비교가 아직 실행 중이었던 경우에는 `cache_miss_reason: null`을 반환합니다.

## 데이터 보존

캐시 진단은 ZDR 적격(조건부)입니다. Anthropic은 이 기능을 위해 프롬프트의 원시 텍스트나 Claude의 출력을 저장하지 않습니다.

API는 `diagnostics` 객체를 포함한 요청에 대해서만 지문을 저장합니다. 지문은 암호화 해시와 토큰 수 추정치로만 구성되며, 응답 `id`를 키로 하고 조직 및 워크스페이스 범위로 한정됩니다. 지문은 짧은 기간 후에 만료되며 다른 어떤 목적으로도 사용되지 않습니다.

모든 기능에 대한 ZDR 적격 여부는 [API 및 데이터 보존](https://platform.claude.com/docs/ko/manage-claude/api-and-data-retention)을 참조하세요.

## 참고 항목

* [프롬프트 캐싱](https://platform.claude.com/docs/ko/build-with-claude/prompt-caching)
* [토큰 카운팅](https://platform.claude.com/docs/ko/build-with-claude/token-counting)
