Prompt caching in sessions¶
This page is part of Sessions: picking up where you left off.
Every model call in a turn re-sends the whole conversation. The provider's prompt cache bills the unchanged prefix at a tenth of the input price, or a twentieth on Opus 5.5 and Sonnet 5.5. A cache entry lives for a fixed time after its last use, 5 minutes by default. Sessions idle for up to 30 minutes, and agents wait on people (needs_input, ask approvals). Returning to a 150k-token conversation on Opus 5.5 costs $0.03 while the cache is warm and $0.75 once it has expired (spec 0012 §6.5). hangar does three things about this:
- it asks for the right TTL;
- it keeps one model per session;
- it lets best-of-N and fork siblings share one cache write.
What it saves shows up in usage (activity.md).
TTL policy¶
cache: auto | 5m | 1h | off. hangar takes the first of these that is set:
- the session's (
new_session(cache=…),POST /api/sessions {"cache": "1h"}); - its environment's (
cache:in the env spec); HANGAR_CACHE(defaultauto).
auto asks for 1h when the session expects to wait more than 5 minutes for its next message:
- its idle time (
idle_minutes, a job'skeep_minutes) is over 5 minutes, or 0 (never suspends); - its tool approvals are
askandHANGAR_APPROVAL_TIMEOUT_SECONDSis over 5 minutes; - its agent has asked a question before (
needs_input). The TTL has to be set before the wait: the turn that asks is the one whose cache entries must survive until the answer arrives.
The harness must also support 1h for the model. Otherwise auto asks for 5m. Each turn records the TTL it asked for in cache_ttl.
The TTL lives on the provider, not in the sandbox. A session that suspends and resumes in a new sandbox within the hour still reads its cache: a resumed Claude Code conversation keeps the system prompt it started with.
| Harness (verified 2026-10-13) | 5m |
1h |
off |
Notes |
|---|---|---|---|---|
| Claude Code 2.1.296 | CLAUDE_CODE_PROMPT_CACHE_TTL=5m |
CLAUDE_CODE_PROMPT_CACHE_TTL=1h |
DISABLE_PROMPT_CACHING=1 |
See below. |
| pi 1.1.0 / 1.2.0 | PI_CACHE_RETENTION=short |
PI_CACHE_RETENTION=long |
no setting: pi's default | long is ttl: "1h" on Anthropic models. On OpenAI models it is 24h retention (GPT-5.5 and earlier) or the 30-minute TTL (GPT-5.6+), where the model has it. |
| Codex 0.162.1 | – | – | – | OpenAI caches automatically. Codex sends prompt_cache_key (the thread id) and no retention option, and has no flag or config key for one. GPT-5.6+ keeps entries at least 30 minutes, the only TTL; writes cost 1.25x input and reads 0.1x (0.05x on gpt-6.1-sol). |
| Antigravity 1.3.3 | – | – | – | No flag, setting or variable. Gemini caches implicitly. |
More on Claude Code:
- Main conversation only. The TTL applies to the main conversation. Subagents keep 5 minutes.
- Its own default. Without the variable, Claude Code asks for 1h only on a subscription within plan usage. An API key or a cloud provider gets 5m.
- What was checked. A scripted Messages API received exactly these:
- by default,
cache_controlwith nottl; - with
CLAUDE_CODE_PROMPT_CACHE_TTL=1h,"ttl": "1h", plus theextended-cache-ttl-2025-04-11beta header; - with
DISABLE_PROMPT_CACHING=1, nocache_controlat all. - LLM gateways. A gateway behind
ANTHROPIC_BASE_URLmust forward thatanthropic-betaheader. - Other providers. On Bedrock, 1-hour availability varies by model; set
cache: 5mthere if requests fail.
Break-even. Let p be the input price and r the cache-read multiplier: 0.05 on Opus and Sonnet 5.5, 0.1 on most others.
- A 1-hour write costs 2p instead of 1.25p, so each token written costs 0.75p more.
- Each gap of 5–60 minutes saves rewriting the context
Cat 1.25p instead of reading it at r·p, a saving of (1.25 − r)·p·C.
So the 1-hour TTL pays for itself once
gaps ≥ 0.75 · W / ((1.25 − r) · C) W = tokens written over the session, C = context at the gaps
| Opus 5.5 ($4/MTok input) | Extra for 1h writes | Saved per 5–60 min gap | Gaps to break even |
|---|---|---|---|
| Nothing expired: W ≈ C = 150k | $0.45 | $0.72 | 0.63, so one gap |
| W = 400k, gaps at C = 150k | $1.20 | $0.72 | 1.7, so two gaps |
| A batch turn that never pauses | +60% on every write | $0 | never, so auto picks 5m |
Gaps longer than an hour lose the cache with either TTL. Use hangar usage --by cache_ttl --cache to check the choice against real turns.
One model per session¶
Each model has its own cache, so switching models makes the next turn re-read the whole conversation uncached. The session's model is fixed when the session is created: the model asked for, else the environment's default for the harness. A later change to the environment doesn't move running conversations.
You can still switch. send_to_session(model=…) (REST POST /api/sessions/{name}/send {"model": …}) switches from that turn on. steer_turn(model=…) (REST …/steer {"model": …}) switches from the next turn, because a running turn keeps its model. Either way:
- the turn gets a warning in
warnings; - the session's events get a
progressevent withdata.warning: "model_switch"; - the audit log records
session.model.
Switching harness (open_session(harness=…)) is a handoff anyway, and takes that harness's default model.
Best-of-N and forks share one cache write¶
Best-of-N attempts, and forks started with a message, begin with the same prompt prefix. Anthropic serves a cache entry only once the first response that writes it has begun. Siblings that all start at once therefore each write the whole prefix.
hangar staggers them instead:
- The siblings boot their sandboxes in parallel.
- The first one to begin its turn goes ahead.
- The others hold the agent's start until that turn shows its first model response in the event stream (a message, a tool call, a command or usage), or until
HANGAR_CACHE_STAGGER_SECONDS(15) after it started. - Each turn records how long it waited in
cache_wait_seconds. - There is no waiting with
cache: off, or after a control-plane restart (the groups live in memory).
Claude Code's own fan-out holds for 5 s. hangar's default is longer because the first sibling must start its CLI in a sandbox, and its response reaches hangar on the next poll.
Four Opus 5.5 attempts on a 100k-token prefix cost $2.00 when each writes it, and $0.56 when one writes and three read.
Best-of-N attempts all get the same prompt (the task, context and protocol); each attempt's branch is named only at the very end. One gap is left, measured with Claude Code 2.1.296: Claude Code opens the first user message with a gitStatus block naming the current branch, ahead of hangar's prompt. Attempts sit on different branches, so for a new best-of-N job the shared prefix ends after the system prompt and tool definitions, and each attempt writes the task text itself (usually a few thousand tokens). CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=1 removes that block (verified), but it also removes Claude Code's commit and PR instructions, so hangar leaves it on. Two cases are not affected:
- Forks of a session that already has a conversation share all of it, because the resumed conversation keeps the git status it started with.
- Codex and pi don't put the branch in the prompt.
Keeping the cache warm: not done¶
A session that waits on a person for over 55 minutes could refresh its 1-hour entry with a one-token request. A refresh is billed as a read: $0.03 for 150k tokens on Opus 5.5. hangar doesn't do this, for two reasons:
- It would miss the cache. The refresh must match the CLI's own request byte for byte: system prompt, tool definitions, beta headers and cache breakpoints. Only the CLI can build that request. A request hangar built would write a new entry, and so cost a write.
- It needs the model credential in the control plane. That credential belongs only in the sandbox (and should eventually leave the sandbox too; spec 0012 item 7).
pi has warming built in (cacheWarming, streaming by default), but its idle mode needs a pi process that outlives the turn. hangar's pi -p turns exit when they finish. Revisit when a CLI offers a refresh command, or when hangar keeps one long-lived CLI process per session.