Skip to content
versa-trainingPublic

About

Find out what expired prompt caches cost you in Claude Code, and keep them warm.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ember

Your coding agent's memory goes cold while it waits. ember keeps it warm, powered by versa

version Claude Code plugin MIT license

33%

of Claude cost goes to expired caches on default API settings
simulated on a heavy multi-agent workload

13.4%

on the same workload with one setting: main agent on 1hr cache TTL

3.7%

with ember keeping main agents and subagents fresh

Your number · The problem · What ember does · Which setup · Commands · For coding agents · Disclaimer

ember is experimental: it depends on cache behavior and prices that Anthropic controls and can change without notice, so run the estimate first. Used wrongly, it can cost you more: see the Disclaimer.

Find out what it costs you

Don't believe my numbers. Ask your agent to estimate your potential cost savings based on your session history.

Run this and see your own number. You need Node 22 or newer on PATH.

git clone https://github.com/versa-training/ember.git
node ember/scripts/estimate-savings.js --days 30

The estimate reads only your local transcripts, starts no agents, and makes no model call.

Ask your agent to interpret the results.

Installation

/plugin marketplace add versa-training/ember
/plugin install ember@ember

Installing adds hooks to keep your agents fresh.

Or tell your agent:

Install the ember plugin from github.com/versa-training/ember and run /ember:benchmark, then tell me which mode to use.

The output looks like this. These are the main agent figures published for the reference workload, trimmed to one block. Every row except actual is simulated, and the wake rows come from a newer run than the others. That workload already ran its main agents on 1 hour, so that row equals actual. A negative saving means the mode costs more.

ember savings estimate, all transcripts, from ~/.claude/projects.
SIMULATED. Nothing was measured and no agent was run. API list prices dated 2026-11-05. Same requests and pauses in every row.

Main agents:
  Simulated cost per mode, saving against actual:
    Actual (measured requests, list prices)        $4,377.05
    5 minutes, no refresh                          $7,127.03     -$2,749.98     -62.8%
    5 minutes, wake every 4:30, up to 4            $4,529.00       -$151.95      -3.5%
    5 minutes, wake every 4:30, up to 24           $4,926.00       -$548.95     -12.5%
    1 hour, no refresh                             $4,377.05         +$0.00       0.0%
    1 hour, wake every 59 min, up to 4             $4,177.00       +$200.05       4.6%

The problem

Claude caches your agent's context. On Opus 5.5 and Sonnet 5.5, a warm read costs 0.05x the input price. Every pause longer than the cache lifetime makes your agent pay to rewrite its whole context: 1.25x on a 5-minute cache, 2x on a 1-hour cache. Your agent pauses whenever it waits for you or for a tool.

The same work, three bills

The third bar comes from an earlier run with unlimited subagent refreshes. With two refreshes the share is still 3.7% (simulated, 38 days to 2026-11-06). With the main agent wake alone it is 10.9%.

These numbers come from a real heavy multi-agent workload, the reference workload: 157,267 requests over 38 days, repriced at API list prices. They are simulated, not a real bill. Your numbers will differ.

The numbers behind the chart
Scenario Main Subagents Total
Everything on 5 minutes $7,127.03 (simulated) $6,417.45 (measured) $13,544.48
Main 1 hour, subagents 5 minutes (actual use) $4,377.05 (measured) $6,417.45 (measured) $10,794.50
Everything on 1 hour $4,377.05 (measured) $6,558.98 (simulated) $10,936.03
  • Dates: 2026-09-29 to 2026-11-05. The same requests and pauses are assumed in every row.
  • Expiries cost $4,468.12 on 5 minutes (33.0%, range 32.1% to 35.0% depending on the expiry assumption) and $1,448.41 on the actual setup (13.4%). Both are simulated.
  • Simulated keepalive on the actual setup, main agent wake only: net $300.03 ($224.46 if sessions are abandoned). Expiry plus wakes are then $1,148 of a $10,494 total (10.9%).
  • With two subagent refreshes as well, on a newer run (38 days to 2026-11-06, total $11,259.70): cost $118.77, expiry avoided $1,195.81, net $1,077.04 ($998.52 if sessions are abandoned), which is 9.6% of the total. Expiry plus wakes are then $377 of a $10,183 total (3.7%). The refreshes are $763.71 of that net.
  • Those figures count every subagent pause. $130.90 of the $917.63 subagent expiry cost (14.3%) does not follow a long Bash timeout, so refreshes may not reach it. Without it the net is $946.14 and the share is 4.9%.
  • The report prints the 5-minute expiry cost as the row "Extra cost of expiries" under "Everything on 5 minutes".

The arithmetic: docs/pricing-math.md.

What ember does

Small hooks, in two parts, both on: wakes for your main agent and refreshes for your subagents.

Hook Who it helps What it does
Subagent refreshes Subagents on a 5-minute cache A command that runs past 4:30 moves to the background, and your subagent refreshes its cache twice: at the move and about 4 minutes later. Shorter commands are not touched. Two refreshes cover 95.9% of subagent pauses on the reference workload. EMBER_SUBAGENT=0 turns this off
Wake hook Your main agent, on a 5-minute or 1-hour cache A wake just before the cache expires (4:30 or 59 minutes), at most 4 between your prompts

The biggest single win needs no plugin. If you use an API key or a cloud provider, set "promptCacheTtl": "1h" in your settings. It needs Claude Code v2.1.242 or later. On the reference workload, it takes the total from $13,544.48 to $10,794.50 (simulated). ember adds less: a net $1,077.04 (simulated, 38 days to 2026-11-06).

Which setup should you use

Your usage Main agent Subagents Why (simulated, reference workload)
Long interactive sessions, API key 1 hour, wake hook optional 5 minutes, subagent refreshes Main agents with 4 or more pauses: $6,413.58 on 5 minutes vs. $3,593.25 on 1 hour
Short one-shot agents 5 minutes 5 minutes Main agents with no pause: $335.68 on 5 minutes vs. $415.39 on 1 hour. 76.8% of main agents and 86.4% of subagents never hit a pause
Mostly subagents that wait often 1 hour subagentPromptCacheTtl at 1h Subagents with 4 or more pauses: $1,238.51 on 5 minutes vs. $703.36 on 1 hour. Past 67.5 minutes of total waiting, the 1-hour premium is cheaper than refreshes. Over all subagents the two are close: $6,417.45 (measured) vs. $6,558.98
Subscription Run the estimate, then decide Run the estimate, then decide Dollars are an equivalent, not a bill. Wakes use plan quota, and the savings are not proven there. EMBER_MAIN=0 turns wakes off, EMBER_SUBAGENT=0 turns subagent refreshes off

Most short agents should stay on 5 minutes, as Anthropic argues. A 1-hour write costs 2x against 1.25x, and it pays off only when your agent comes back. The rule the math supports: main agent on 1 hour with an optional wake at 59 minutes, subagents on 5 minutes with two refreshes during a long command. See docs/pricing-math.md.

Benchmark your own usage

/ember:benchmark prices each cache mode on your own history and shows how often your agents pause. Method, modes, and limits: docs/benchmarks.md.

Commands

Command What it does
/ember:benchmark Runs scripts/check-env.js and scripts/estimate-savings.js and interprets them. Reads history only, starts no agent
/ember:report Prints the cost report
/ember:wakes <count> Sets the wake limit for this session. You can also tell your agent, for example "12 wakes". 0 turns wakes off
node scripts/estimate-savings.js --days N Estimates each cache mode on your history (simulated, free)
node scripts/cache-report.js --days N --dir PATH --json Runs the report from a clone
node scripts/check-env.js Prints the Claude Code version, the cache lifetime in recent transcripts, and relevant environment variables

Configuration

ember reads the cache lifetime from your Claude Code configuration on every hook call, separately for the main agent and for subagents. Nothing is stored. Leave the EMBER_* lifetime overrides unset: they exist for testing.

Main agent lifetime, first match:

  1. EMBER_MAIN_TTL (5m or 1h, for testing)
  2. The newest cache write in the transcript, because it is what happened
  3. FORCE_PROMPT_CACHING_5M=1
  4. CLAUDE_CODE_PROMPT_CACHE_TTL, in the environment, then in the env block of a settings file
  5. promptCacheTtl in a settings file
  6. ENABLE_PROMPT_CACHING_1H=1
  7. Unknown: the plan decides the default, so ember waits for the first cache write

Subagent lifetime, first match: EMBER_SUBAGENT_TTL, FORCE_PROMPT_CACHING_5M=1, CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, subagentPromptCacheTtl, ENABLE_PROMPT_CACHING_1H=1, then 5m. A cacheTtl in a subagent's frontmatter is not read.

Settings files, first match: /etc/claude-code/managed-settings.json, <cwd>/.claude/settings.local.json, <cwd>/.claude/settings.json, ~/.claude/settings.json. Files are read again on every call. A missing or malformed file is skipped, and a value other than 5m or 1h is ignored.

Wake limit, first match: the limit set for the session with /ember:wakes, EMBER_MAX_WAKES, then 4. To change the default for every session, set EMBER_MAX_WAKES in the env block of your settings. A session limit stays until the session ends.

Intervals follow the lifetime: 5 minutes gives 270 seconds, 1 hour gives 3540 seconds. A sleeping wake hook resolves the lifetime again about once a minute and just before it wakes. If you change a settings file while it sleeps, the deadline moves, counted from the hook's start. Limit: a hook keeps the environment it started with, so a variable changed during a session is not seen. Settings files are.

node scripts/check-env.js prints the lifetime, interval, and wake limit per agent type with the source of each value. When the transcript and the configuration disagree, it says so.

Environment variables
Variable Default Effect
EMBER_MAIN=0 on Turns off the Stop hook wakes
EMBER_SUBAGENT=0 on Turns off subagent refreshes
EMBER_MAIN_TTL, EMBER_SUBAGENT_TTL unset Test overrides of the resolved lifetime (5m or 1h). Leave them unset
EMBER_SUBAGENT_WAIT_MS from the lifetime: 270000 or 3540000 A longer foreground Bash timeout in a subagent is lowered to this. Overrides exactly
EMBER_MAIN_INTERVAL_SEC from the lifetime: 270 or 3540 Seconds between wakes. Overrides exactly. Setting it skips the transcript wait, and a headless run may then wait for a pending sleeper before it exits
EMBER_MAX_WAKES 4 Wakes allowed before the limit resets. A limit set with /ember:wakes wins for that session
EMBER_WAKE_MESSAGE ember keepalive: ... Text of the wake turn
EMBER_STATE_DIR ~/.claude/ember Where wake state is stored. It must be a real directory you own, not a symlink, or the hooks do nothing
EMBER_LOG, EMBER_RESOLVE_MS, EMBER_TRANSCRIPT_ROOT unset Testing aids: a file that records each sleeper deadline, the re-resolve period in milliseconds, and the folder check-env.js reads transcripts from

For coding agents

  • Read AGENTS.md first.
  • Use the scripts for all arithmetic and parsing. Never compute figures by hand.
  • Never start agents or sessions to benchmark. The estimate reads history only.
  • Label figures as simulated at API list prices, and give the dates. Say that your user's figures will differ from the reference workload.

How it works

Subagent refreshes are two hooks. A PreToolUse hook lowers a foreground Bash timeout above the subagent's interval to that interval (270 seconds on a 5-minute cache), because Claude Code does not move a command with a long declared timeout to the background. A PostToolUse hook sees the move and tells the subagent to check once. The check waits without using tokens, and the request after it refreshes the cache. So a long command gets two refreshes: at the move and after the check. The wake hook is a Stop hook with asyncRewake. It sleeps one interval (270 seconds or 59 minutes), then wakes the session for a one-word turn, which is a real turn in your transcript.

Hook details and test status

Subagent refreshes

  • It does nothing when CLAUDE_CODE_DISABLE_BACKGROUND_TASKS is set to a truthy value, because the instruction cannot be followed.
  • On a 1-hour subagent lifetime the limit is 3540 seconds, so a timeout is almost never lowered.
  • The instruction includes the check command, because a call that starts with sleep is rejected by Claude Code and a subagent that ends its turn waits for the finish notification without a request. It sets a Bash timeout, because a check without one would itself move to the background at 120 seconds.
  • A foreground subagent that runs longer than the main agent's lifetime cannot be helped: the main agent is inside one turn, so no Stop hook fires and its cache expires.
  • A command that goes past its timeout moves to the background and keeps running. No permission decision is set, so the normal permission check still applies.
  • A command you start with run_in_background gets no instruction, because it may be a long-lived service. Two refreshes keep the cache for about 13 minutes and cover 95.9% of subagent pauses on the reference workload. A longer command can still expire after that.

Wake hook

  • It wakes at most 4 times before the limit resets, on a 5-minute or 1-hour main lifetime. A subagent cannot be woken by a hook: a hook wake goes to the main session only (tested).
  • The limit resets on every UserPromptSubmit event, except the one a wake itself fires. A task notification fires that event too, so it also resets the limit (tested below).
  • The sleep ends one interval after the hook starts, because the transcript at Stop can lack the current turn. A final response that streams for more than 60 seconds can therefore let the cache expire before the wake (about 0.13% of final turns on the reference workload).
  • A newer Stop or prompt ends the sleep within about 5 seconds.
  • A wake that would come more than 60 seconds late, for example after a laptop suspend, is skipped because the cache has probably expired already.

Tested. Unit tests pass (npm test). The live tests below ran under the earlier name warmcache on the same code, against Claude Code:

  • asyncRewake woke a headless stream-json session on Claude Code 2.1.310 with a 12-second interval, and the wake limit held (2 wakes with a limit of 2, then none).
  • A review confirmed that claude plugin validate passes, that --plugin-dir loads the plugin on 2.1.310, and that a wake fired and a limit of 1 held in a live headless run.
  • In one-shot -p mode the transcript at Stop lacked the current turn, so no sleeper started.
  • An interactive session with a short interval showed 4 wakes and the limit of 4 held. The UI displays each wake as "Stop hook feedback".
  • Wake timing, 2.1.310, interactive, interval 60 seconds: before 0.1.3 the second wake came 1 second after the first, because the transcript at Stop lacked the current turn. After the fix the hooks started 60.9 and 60.8 seconds apart. One run each.

Live tests of 0.3.0. Claude Code 2.1.310, haiku, headless stream-json, 5-minute lifetime, the real 270-second interval, one run each. Tables: results in the ember-test repository.

  • Main idle 17 minutes with a limit of 3: 3 wakes at 272, 271, and 271 seconds after the previous request, each a cache hit, then none. With EMBER_MAIN=0, no wake in 11 minutes.
  • Subagent, EMBER_SUBAGENT=0: the foreground call ran 482 seconds and the next request rewrote the full context (cache read 0).
  • Main waiting on a background subagent: the main wake fired at 272 seconds as a cache hit while the subagent ran, and the finish notification woke the main agent normally. The idle background subagent expired its own cache (gap 353 seconds, full rewrite).
  • A foreground subagent longer than 5 minutes: the main agent's next request came 489 seconds later with a full rewrite, because no Stop hook fires during a turn.
  • Configuration: with both lifetimes at 1h, check-env.js and the first sleeper deadline gave 3540 seconds and a 590000 ms subagent timeout was allowed. Editing promptCacheTtl in a settings file from 5m to 1h while a sleeper ran moved its deadline from 270 to 3540 seconds and check-env.js followed.
  • A hook cannot wake a subagent. A throwaway asyncRewake hook on PreToolUse and SubagentStop woke the main session only.

Live tests of 0.4.0. Claude Code 2.1.310, haiku, headless stream-json, the plugin installed at user scope, EMBER_MAIN_INTERVAL_SEC of 6, one run each.

  • EMBER_MAX_WAKES=6 in the env block of the user settings: 6 wakes, then none.
  • A session told "I only want 2 wakes" used /ember:wakes and got 2 wakes, then none.
  • EMBER_SUBAGENT=0: a subagent's foreground Bash call with a 600000 ms timeout ran unchanged.
  • Task notifications, --plugin-dir, interval 25 seconds, limit of 3: 3 wakes, a notification, then 3 more wakes.

Live tests of 0.6.0, subagent refreshes. Haiku, headless stream-json, 5-minute subagent lifetime, a 460 to 560 second command unless noted, one run each.

  • Foreground subagent on 2.1.310, background subagent on 2.1.310, and foreground subagent on 2.1.318, all with a 600000 ms timeout: the command moved at 270 seconds, the subagent checked until it ended (an earlier text with no limit of one check), the longest gap was 273 seconds, and every request after a gap was a cache hit.
  • No declared timeout on 2.1.310: the command moved at 120 seconds, the longest gap was 243 seconds, all cache hits.
  • A 20-second command with a 600000 ms timeout: returned normally, with no move and no check.
  • Control with the hooks off: the 600000 ms call ran 560 seconds and the next request rewrote the context. With no timeout, the command moved at 120 seconds, the subagent went idle, and after a 434-second gap the next request read 0 from the cache.
  • The limit of one check, on 2.1.310 with the cap set to 60 seconds and a 150-second command: the command moved at 60 seconds, the subagent checked once and ended its turn, and the finish notification woke it. Every request was a cache hit.

Untested. Subagent refreshes in an interactive session, and on models other than haiku. Claude Code handles the move and the hooks the same way for any model, and a model that ignores the instruction only loses the refresh. Laptop suspend, /clear with a pending sleeper, an interactive session at the 270-second interval (the launch was blocked by this machine's agent tooling), the real 59-minute interval, and a settings change made while a live Claude Code session ran.

Disclaimer

Anthropic controls what ember depends on. Cache lifetimes, prices, and hook behavior can change at any time, without notice. Used wrongly, ember can cost you more, not less. Run the report before and after any Claude Code upgrade.

This has happened before. Users measured Claude Code cache writes moving from 1 hour to 5 minutes around early March 2026, with no announcement they could find (issue 46829, closed as not planned).

Anthropic's reply. In that issue, Jarred-Sumner, writing for the Claude Code team, describes the change as deliberate optimization, not a regression. The points:

  • 1-hour writes cost more (2x vs. 1.25x) and do not pay off for one-shot requests.
  • The client selects a lifetime per request.
  • The change lowers cost across the request mix.

The commenter bcherny, whose GitHub profile says "Claude Code @ Anthropic", wrote: "we use experiment gates that are cached client-side" and "We also are not defaulting API customers to 1h yet". bcherny also wrote that the savings are "nowhere near 12x" and "a small win".

A later report. Issue 84253, still open, saw 5-minute-only writes from 2.1.218. Later comments in that thread say the remainder was a detection gap. A subscription token passed through ANTHROPIC_AUTH_TOKEN gets the API-key default. One commenter measured 1 hour on all 29 versions tested with a plain subscription login.

What the data can and cannot show. ember's report charges every write at the lifetime's price, so one-shot requests are counted. On the reference workload, 76.8% of main agents and 86.4% of subagents never hit a pause of 5 to 60 minutes, and 5 minutes is the right choice for them. That agrees with Anthropic's argument. Anthropic's claim covers all users, which one workload cannot test.

Newer versions may already cover part of this.

  • Claude Code 2.1.318 changelog: "Changed background subagents to move a shell command to the background after 3 minutes on all providers, so the prompt cache doesn't expire while it runs". In a test on 2.1.318 a command with a 600000 ms timeout did not move in 220 seconds, and a command with no timeout moved at 120 seconds. So ember still lowers long timeouts.
  • Claude Code 2.1.314 changelog: "Added idle compaction to VS Code and Claude Desktop's Code tab: a 200K+ token conversation left idle is summarized, and the summary is used on your next message if the prompt cache has expired".

The figures are simulated. They come from one workload at API list prices. They are not a real bill, and yours will differ. No result is guaranteed.

Subscriptions. Wakes use plan quota. How quota weights cached reads is unknown. The savings are not proven for subscriptions.

No affiliation. This project is not affiliated with Anthropic.

Privacy

Everything stays local. The plugin code makes no network calls and collects no telemetry. Wakes and the report's chat output go to the model like any other turn. The hooks store wake state in ~/.claude/ember. scripts/check-env.js leaves out variables with secret-looking names.

Similar projects

Development

npm test

See CONTRIBUTING.md to change the code, CHANGELOG.md for releases, and AGENTS.md for coding agents.

Credits

The logo and chart use the font Patrick Hand by Patrick Wagesreiter (SIL Open Font License); see NOTICE.

About

ember is from the team behind versa.training. Looking for a tool to help your team master difficult conversations? Check out versa.training.

License

MIT


An ember stays warm between fires. ember keeps your prompt cache warm between turns.

Pricing math · Benchmarks · Changelog · Contributing · Issues

About

Find out what expired prompt caches cost you in Claude Code, and keep them warm.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages