from line 3
33url: https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics
44description: Diagnose unexpected prompt cache misses by comparing consecutive requests and identifying exactly where the prompt prefix diverged.
55featureMetadata:
6 status: beta
7 betaHeader: cache-diagnosis-2026-04-07
6 status: ga
87 zdr:
98 eligibility: eligible
109 note: Excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements).
1110 supportedPlatforms:
12 Claude API: beta
11 Claude API: ga
1312 Claude Platform on AWS: not available
1413 Amazon Bedrock: not available
1514 Google Cloud: not available
from line 21
2221
2322## How cache diagnostics works
2423
25When the beta header is present, the API stores a lightweight fingerprint of each request, keyed by the response `id`. On your next request, include that `id` as `diagnostics.previous_message_id`. The API rebuilds the fingerprint for the new request, compares it against the stored one, and attaches a `diagnostics` object to the response describing the first point of divergence.
24For each request that includes the `diagnostics` object, the API stores a lightweight fingerprint keyed by the response `id`. It stores nothing for requests that omit the object. On your next request, include the previous response's `id` as `diagnostics.previous_message_id`. The API rebuilds the fingerprint for the new request, compares it against the stored one, and attaches a `diagnostics` object to the response describing the first point of divergence.
2625
2726The comparison is about request structure, independent of whether the cache actually hit. See [Reading diagnostics alongside usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#reading-diagnostics-alongside-usage) for how to combine the `diagnostics` result with `usage.cache_read_input_tokens`.
2827
from line 29
3029
3130## Basic usage
3231
33Send the beta header on every turn. On the first turn, pass `"previous_message_id": null` to opt in without a prior message to compare against. On subsequent turns, pass the `id` from the previous response.
32Include the `diagnostics` object on every turn. The object is the opt-in: the API stores a fingerprint only for requests that include it. On the first turn, pass `"previous_message_id": null` to opt in without a prior message to compare against. On subsequent turns, pass the `id` from the previous response. The `cache-diagnosis-2026-04-07` beta header is no longer required, and requests that still send it work as before.
3433
3534<CodeGroup>
3635 ```bash cURL
from line 37
3837 response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
3938 -H "x-api-key: $ANTHROPIC_API_KEY" \
4039 -H "anthropic-version: 2023-06-01" \
41 -H "anthropic-beta: cache-diagnosis-2026-04-07" \
4240 -H "content-type: application/json" \
4341 -d '{
4442 "model": "claude-opus-5-5",
from line 53
5553 curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
5654 -H "x-api-key: $ANTHROPIC_API_KEY" \
5755 -H "anthropic-version: 2023-06-01" \
58 -H "anthropic-beta: cache-diagnosis-2026-04-07" \
5956 -H "content-type: application/json" \
6057 -d @- <<EOF | jq '{id, diagnostics}' # diagnostics: null means no divergence was found
6158 {
from line 73
7673 ```bash CLI
7774 # Turn 1
7875 turn1=$(ant beta:messages create \
79 --beta cache-diagnosis-2026-04-07 \
8076 --transform '{id,usage,diagnostics}' <<'YAML'
8177 model: claude-opus-5-5
8278 max_tokens: 1024
from line 91
9591 # Turn 2: pass the id from turn 1 as previous_message_id
9692 message_id=$(jq -r '.id' <<<"$turn1")
9793 ant beta:messages create \
98 --beta cache-diagnosis-2026-04-07 \
9994 --transform '{id,usage,diagnostics}' <<YAML
10095 model: claude-opus-5-5
10196 max_tokens: 1024
from line 122
127122 system=SYSTEM,
128123 messages=[{"role": "user", "content": "Summarize section 1."}],
129124 diagnostics={"previous_message_id": None},
130 betas=["cache-diagnosis-2026-04-07"],
131125 )
132126
133127 # Turn 2: reference the previous response id
from line 136
142136 {"role": "user", "content": "Now summarize section 2."},
143137 ],
144138 diagnostics={"previous_message_id": r1.id},
145 betas=["cache-diagnosis-2026-04-07"],
146139 )
147140
148141 diagnostics = r2.diagnostics
from line 159
166159 cache_control: { type: "ephemeral" },
167160 system: SYSTEM,
168161 messages: [{ role: "user", content: "Summarize section 1." }],
169 diagnostics: { previous_message_id: null },
170 betas: ["cache-diagnosis-2026-04-07"]
162 diagnostics: { previous_message_id: null }
171163 });
172164
173165 // Turn 2: reference the previous response id
from line 173
181173 { role: "assistant", content: r1.content },
182174 { role: "user", content: "Now summarize section 2." }
183175 ],
184 diagnostics: { previous_message_id: r1.id },
185 betas: ["cache-diagnosis-2026-04-07"]
176 diagnostics: { previous_message_id: r1.id }
186177 });
187178
188179 if (r2.diagnostics === null) {
from line 202
211202 new() { Role = Role.User, Content = "Summarize section 1." },
212203 ],
213204 Diagnostics = new() { PreviousMessageID = null },
214 Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
215205 }
216206 );
217207
from line 223
233223 new() { Role = Role.User, Content = "Now summarize section 2." },
234224 ],
235225 Diagnostics = new() { PreviousMessageID = r1.ID },
236 Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
237226 }
238227 );
239228
from line 253
264253 Diagnostics: anthropic.BetaDiagnosticsParam{
265254 PreviousMessageID: param.Null[string](),
266255 },
267 Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
268256 })
269257 if err != nil {
270258 panic(err)
from line 271
283271 Diagnostics: anthropic.BetaDiagnosticsParam{
284272 PreviousMessageID: anthropic.String(r1.ID),
285273 },
286 Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
287274 })
288275 if err != nil {
289276 panic(err)
from line 300
313300 .addUserMessage("Summarize section 1.")
314301 // Pass null on the first turn to opt in without a prior message to compare.
315302 .diagnostics(BetaDiagnosticsParam.builder().previousMessageId((String) null).build())
316 .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
317303 .build()
318304 );
319305
from line 313
327313 .addMessage(r1)
328314 .addUserMessage("Now summarize section 2.")
329315 .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
330 .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
331316 .build()
332317 );
333318
from line 343
358343 ['role' => 'user', 'content' => 'Summarize section 1.'],
359344 ],
360345 diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID(null),
361 betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
362346 );
363347
364348 $r2 = $client->beta->messages->create(
from line 356
372356 ['role' => 'user', 'content' => 'Now summarize section 2.'],
373357 ],
374358 diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
375 betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
376359 );
377360
378361 echo match (true) {
from line 378
395378 messages: [
396379 {role: "user", content: "Summarize section 1."}
397380 ],
398 diagnostics: {previous_message_id: nil},
399 betas: ["cache-diagnosis-2026-04-07"]
381 diagnostics: {previous_message_id: nil}
400382 )
401383
402384 r2 = client.beta.messages.create(
from line 391
409391 {role: "assistant", content: r1.content},
410392 {role: "user", content: "Now summarize section 2."}
411393 ],
412 diagnostics: {previous_message_id: r1.id},
413 betas: ["cache-diagnosis-2026-04-07"]
394 diagnostics: {previous_message_id: r1.id}
414395 )
415396
416397 case r2.diagnostics
from line 416
435416 curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
436417 -H "x-api-key: $ANTHROPIC_API_KEY" \
437418 -H "anthropic-version: 2023-06-01" \
438 -H "anthropic-beta: cache-diagnosis-2026-04-07" \
439419 -H "content-type: application/json" \
440420 -d @- <<EOF | jq -R 'select(startswith("data: ")) | ltrimstr("data: ") | fromjson | select(.type == "message_start") | .message.diagnostics'
441421 {
from line 438
458438 # Turn 2: stream. With --stream the CLI emits each SSE event as one JSON object.
459439 # diagnostics arrives on the message_start event; pick it out with jq.
460440 ant beta:messages create \
461 --beta cache-diagnosis-2026-04-07 \
462441 --stream --format jsonl <<YAML |
463442 model: claude-opus-5-5
464443 max_tokens: 1024
from line 470
491470 {"role": "user", "content": "Now summarize section 2."},
492471 ],
493472 diagnostics={"previous_message_id": r1.id},
494 betas=["cache-diagnosis-2026-04-07"],
495473 ) as stream:
496474 for text in stream.text_stream:
497475 print(text, end="", flush=True)
from line 496
518496 { role: "assistant", content: r1.content },
519497 { role: "user", content: "Now summarize section 2." }
520498 ],
521 diagnostics: { previous_message_id: r1.id },
522 betas: ["cache-diagnosis-2026-04-07"]
499 diagnostics: { previous_message_id: r1.id }
523500 });
524501
525502 for await (const event of stream) {
from line 540
563540 new() { Role = Role.User, Content = "Now summarize section 2." },
564541 ],
565542 Diagnostics = new() { PreviousMessageID = r1.ID },
566 Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
567543 }
568544 );
569545
from line 580
604580 Diagnostics: anthropic.BetaDiagnosticsParam{
605581 PreviousMessageID: anthropic.String(r1.ID),
606582 },
607 Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
608583 })
609584 defer stream.Close()
610585
from line 615
640615 .addMessage(r1)
641616 .addUserMessage("Now summarize section 2.")
642617 .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
643 .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
644618 .build();
645619
646620 var accumulator = BetaMessageAccumulator.create();
from line 655
681655 ['role' => 'user', 'content' => 'Now summarize section 2.'],
682656 ],
683657 diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
684 betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
685658 );
686659
687660 $diagnostics = null;
from line 692
719692 {role: "assistant", content: r1.content},
720693 {role: "user", content: "Now summarize section 2."}
721694 ],
722 diagnostics: {previous_message_id: r1.id},
723 betas: ["cache-diagnosis-2026-04-07"]
695 diagnostics: {previous_message_id: r1.id}
724696 )
725697
726698 stream.each do |event|
from line 754
782754 system=SYSTEM,
783755 messages=messages,
784756 diagnostics={"previous_message_id": prev_id},
785 betas=["cache-diagnosis-2026-04-07"],
786757 )
787758
788759 if r.diagnostics is not None and r.diagnostics.cache_miss_reason is not None:
from line 784
813784 cache_control: { type: "ephemeral" },
814785 system: SYSTEM,
815786 messages,
816 diagnostics: { previous_message_id: prevId },
817 betas: ["cache-diagnosis-2026-04-07"]
787 diagnostics: { previous_message_id: prevId }
818788 });
819789
820790 if (r.diagnostics?.cache_miss_reason) {
from line 820
850820 System = system,
851821 Messages = messages,
852822 Diagnostics = new() { PreviousMessageID = prevId },
853 Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
854823 }
855824 );
856825
from line 866
897866 Diagnostics: anthropic.BetaDiagnosticsParam{
898867 PreviousMessageID: prevID,
899868 },
900 Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
901869 })
902870 if err != nil {
903871 panic(err)
from line 907
939907 .system(system)
940908 .messages(messages)
941909 .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(prevId).build())
942 .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
943910 .build()
944911 );
945912
from line 944
977944 system: $system,
978945 messages: $messages,
979946 diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($prevId),
980 betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
981947 );
982948
983949 if ($r->diagnostics?->cacheMissReason !== null) {
from line 974
1008974 cache_control: {type: "ephemeral"},
1009975 system_: SYSTEM,
1010976 messages: messages,
1011 diagnostics: {previous_message_id: prev_id},
1012 betas: ["cache-diagnosis-2026-04-07"]
977 diagnostics: {previous_message_id: prev_id}
1013978 )
1014979
1015980 if (reason = r.diagnostics&.cache_miss_reason)
from line 990
1025990
1026991## Response format
1027992
1028The `diagnostics` field on the response `Message` has four possible states:
993The `diagnostics` field on the response `Message` has three possible values:
1029994
1030995| Value | Meaning |
1031996| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1032| field absent | The request did not include `diagnostics`, or the beta header was missing. |
1033| `null` | Either `previous_message_id` was `null` (first turn, nothing to compare), or a comparison ran and found no divergence. |
997| `null` | The request did not include the `diagnostics` object, `previous_message_id` was `null` (first turn, nothing to compare), or a comparison ran and found no divergence. |
1034998| `{"cache_miss_reason": null}` | The comparison was still running when the response was serialized. This can happen when the response starts very quickly. Treat it as inconclusive and check the next turn. |
1035999| `{"cache_miss_reason": {...}}` | A `cache_miss_reason` is attached. For `*_changed` types this identifies the first divergence point; `previous_message_not_found` and `unavailable` are cases where no comparison was produced. |
10361000
from line 1031
10671031| `system_changed` | The `system` parameter differs. Typically a timestamp, request ID, or other per-request value was interpolated into the system prompt. | Make the system prompt a byte-stable constant and move dynamic data into the first `user` message after your cache breakpoint. |
10681032| `tools_changed` | The `tools` array differs: tools were added, removed, or reordered between turns, or tool `input_schema` JSON was serialized non-deterministically. | Send the same tool list on every turn in a fixed order with deterministically serialized schemas (for example, sort keys). |
10691033| `messages_changed` | The model, system, and tools all match, but an earlier entry in `messages` was altered, reordered, or removed rather than appended to. Typically conversation history was truncated or edited, or assistant turns and `tool_result` blocks were re-serialized differently on resend. | Treat the history as append-only; echo assistant `content` and tool results back verbatim. |
1070| `previous_message_not_found` | No stored fingerprint exists for the supplied `previous_message_id`. This is not evidence that your request changed. Typically the previous request did not carry the beta header, it came from a different workspace, or too much time has passed since it was sent. | Send the beta header on every turn and keep consecutive turns close together in time. |
1034| `previous_message_not_found` | No stored fingerprint exists for the supplied `previous_message_id`. This is not evidence that your request changed. Typically the previous request did not include the `diagnostics` object, it came from a different workspace, or too much time has passed since it was sent. | Include the `diagnostics` object on every turn and keep consecutive turns close together in time. |
10711035| `unavailable` | Diagnostic information was not available for this request. This includes the case where `model`, `system`, and `tools` match but another prompt-affecting request parameter (`tool_choice`, `thinking`, `context_management`, `output_config`, `output_format`, or the set of active `anthropic-beta` headers) differs, and very long conversations where the divergence is beyond the comparison horizon. Your request was processed normally. | Keep the prompt-affecting request parameters constant for the lifetime of a cached conversation. If persistent, apply the manual checks under [Troubleshooting common issues](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#troubleshooting-common-issues) on the prompt caching page. |
10721036
10731037<Note>
from line 1053
10891053
10901054## Limitations
10911055
1092* **Beta:** Field names and semantics may change while this feature is in beta.
10931056* **Claude API only:** Not available on Amazon Bedrock or Google Cloud.
10941057* **Limited retention:** Fingerprints for `previous_message_id` lookup expire after a short period. Run diagnostic comparisons between closely spaced requests.
10951058* **Same workspace:** The previous request must have run in the same organization and workspace. To check, compare the `anthropic-workspace-id` [response header](https://platform.claude.com/docs/en/api/overview#response-headers) on the two responses.
from line 1063
11001063
11011064Cache diagnostics is ZDR eligible (qualified). Anthropic does not store the raw text of your prompts or Claude's outputs for this feature.
11021065
1103The fingerprint stored for each request consists only of cryptographic hashes and token-count estimates, keyed by the response `id` and scoped to your organization and workspace. Fingerprints expire after a short period and are not used for any other purpose.
1066The API stores a fingerprint only for requests that include the `diagnostics` object. The fingerprint consists only of cryptographic hashes and token-count estimates, keyed by the response `id` and scoped to your organization and workspace. Fingerprints expire after a short period and are not used for any other purpose.
11041067
11051068For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention).
11061069
from line 1071
11081071
11091072* [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
11101073* [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting)
1111* [Beta headers](https://platform.claude.com/docs/en/api/beta-headers)
11121074