A timestamp in your system prompt costs you the whole cache, every call
Prompt caches match on exact bytes, so a clock in your prefix misses every time — and the detector we wrote to catch it was so eager it switched caching off for the word "Tuesday".
Prompt caching is a prefix match on bytes. Not a semantic match, not a fuzzy one: the provider hashes the front of your request, and if the hash is the same as last time, it reads the tokens back instead of processing them. Change one character anywhere in that prefix and there is nothing to read.
Which is why this line, sitting at the top of a system prompt, is expensive:
You are a support agent. The current time is 2026-09-15T09:14:22Z.Every call rewrites the prefix. Every call misses. And a miss is not neutral, because a cache write is not free: on Anthropic a write costs about 1.25x the normal input rate and a read costs about 0.1x. A prefix that writes on every call and never reads is therefore around 25% more expensive than never caching at all. You are paying a premium to fill a cache that is dead before the next request arrives.
The detector, and the way it was wrong
Vigil looks for this. The proxy scans the cacheable prefix — the system prompt and the tool definitions, never the user messages — and when it finds a volatile value it either splits the cached span before it, or declines to cache and tells you why.
The first version of that detector matched the words. Alongside the patterns for real dates and times it had two regexes that did nothing but look for time vocabulary: one for the phrases timestamp, as of, now is, today is, current date and current time, and one for every bare weekday name. The reasoning was written down at the time, and it was this: a false positive only forgoes a cache, which is free, while a false negative charges the customer. Detect eagerly.
One afternoon of traffic produced four of them.
"the Last updated timestamp field" → system prompt declared volatile
"what a good Tuesday looks like" → system prompt declared volatile
"the last successful sync timestamp" → in a TOOL DESCRIPTION
"as of the last release" → same bug, had not fired yetLook at what those actually are. A field name. A sentence about how a work queue behaves. A tool that reports when a connector last synced. A reference to a past release. Not one of them is a value that changes between two calls. All four are the same bytes on every request, which is the definition of cacheable.
The tool-description case is the one that costs money
The third line is a different order of problem from the first two, and it is why "eager detection is free" was not merely imprecise but backwards.
Tool definitions render before the system prompt. They sit inside every cached span there is, which means a hit inside a tool description cannot be split off — there is no prefix left in front of it to keep. So the proxy does the only thing it can: it returns volatile_tools and the entire prefix goes uncached.
We logged six of those rows. Six calls where one noun, in one tool description, in a prompt that had not changed in weeks, switched caching off completely. Not degraded — off. And silently: the agent kept working, the responses kept coming, and the only signal was a savings figure that stayed at zero for a customer who had opted in.
In the system prompt it is cheaper but the same shape. The split lands at the phantom match, and everything after it — usually most of the prompt — stops being cacheable.
So the original trade was wrong in both directions at once. A missed timestamp costs one 1.25x write that is never read back. A false positive costs the entire saving on every subsequent call, and reads to the customer as the product simply not working. The false positive is not the cheaper error. It is much the more expensive one.
The rule that replaced it
Fire only on something that is recognisably a formatted date or time on its own — a value whose bytes differ between two calls minutes or days apart. A noun that describes time, a label that introduces one, and a weekday name in prose are all stable text.
Both word-matching regexes were deleted. What remains is ten patterns that match values: ISO datetimes, unix epochs, <current_time> tags, text dates with and without a year, a weekday anchored to a date, HH:MM clocks, slash and dot calendar dates, European 14h07 clocks, and am/pm times.
Narrowing those to values and nothing else turned out to be most of the work, and the near-misses are the interesting part:
- May is excluded from the year-less date forms. It is the one month name that is also a common English auxiliary verb, so "Item 3 may require approval" reads as a date to any pattern that takes a number before a month name. Where a four-digit year is present the construction is unambiguous and May is allowed back in.
- Year-less text dates are day-first only.
16 Septemberfires;September 16does not, because "May 3 approvals are required" and "March 12 items" are identical in shape. - Year-less months are case-sensitive. Dates capitalise the month. "deliver 16 september" does not, and stays quiet.
- A weekday needs a date next to it.
Tuesday, September 16fires.every Tuesdaydoes not. That anchor is also what makes the month-first form safe there, since the weekday carries the ambiguity the bare form could not. - A clock time has to be a real clock value. Hour 0–23, minute 00–59, so
aspect ratio 16:9anda 1:75 scale modelare not times. The am/pm form is bounded to 1–12, so "run 40 pm tasks" is not either.
One more detail that matters more than it looks: when several patterns match, the detector takes the earliest match in the string, not the first regex that happens to hit. The split point in the system prompt is that index, so first-regex-wins would cut the prompt in the wrong place and throw away cacheable text sitting in front of the real timestamp.
What this deliberately gives up
A prompt carrying only a bare weekday and no date — "Today is Tuesday." — changes daily and is no longer caught. That is the stated price of not flagging "every Tuesday", and we think it is the right one: the empirical check is what should be catching volatility that regexes have to guess at. Vigil hashes the prefix on every call, so a prompt whose bytes actually move shows up as a hash that never repeats, whatever the reason. A pattern that guesses is a worse instrument than a measurement that does not have to.
Why this is the most common cache failure
Because injecting the current time into a system prompt is good practice for an agent. The model needs to know what day it is to reason about deadlines, schedules and "latest". Every agent framework that has thought about it does this, and several do it in the first line of the prompt, which is the worst possible position: it invalidates everything behind it.
The fix is almost always to move it rather than remove it. The model needs the clock; the cache needs the prefix stable. Those are compatible as long as the volatile value sits after everything you want cached — at the end of the system prompt, or better, in the first user message, which is not part of the cacheable prefix at all.
What to do
Read your own system prompt and tool definitions and find every value that differs between two calls a minute apart: clocks, dates, session and request ids, trace ids, anything with an epoch in it. Move each one behind the text you want cached, or into the first user message. Then check two things people usually miss — that your prefix clears your model's minimum (it is 1024 tokens on Sonnet 4.6, 512 on Opus 5, and it is not monotonic across generations, so look yours up rather than assuming), and that your tool array is in a stable order, since tools sit at the very front of the span and reordering them busts it just as thoroughly as a clock does.
If you route through Vigil, the Optimize page names the reason per agent, and volatile_tools is the one to look for first: it means nothing is being cached at all.