Skip to content

Context Caching in the Gemini API: The Minimum Token Floor and TTL

9 min read · updated August 11, 2026

Explicit context caching in the Gemini API is a resource you create, pay rent on, and reference by name. It refuses inputs below a documented token floor, and that refusal is not arbitrary—it falls out of what is being stored.

What an explicit cache actually stores

When a model reads a prompt, it computes internal key and value tensors for every token—the KV cache. Generation then reuses them. Those tensors are what makes prefill expensive and they are entirely determined by the tokens, the model and the model’s version.

A CachedContent resource is that computation, kept. You upload content once, Google holds the derived state, and subsequent requests reference it by name instead of resending the tokens for recomputation. Two things follow immediately, and they explain nearly every rule on this page:

  • The stored artefact is large—proportional to the token count times the model’s hidden size times its layer count. It occupies real accelerator-adjacent memory for as long as it lives, which is why it is billed by token-hour rather than being free.
  • It is model-specific and version-specific. A cache built for one model version is meaningless to another, because the tensors were produced by those particular weights.

The minimum, and why there is one

Below some size, a cache costs more to create, store and look up than the prefill it saves, so the API declines to make one. Google documents a minimum input token count for cacheable content in its context caching guide, as a per-model figure in the table on that page.

For the Gemini 1.5 generation that figure was 32,768 tokens—a large floor, and one that ruled out caching a typical system prompt. Google lowered it for later generations, and the current numbers are per-model rather than per-API.

The minimum cacheable token count has changed between the 1.5, 2.0 and 2.5 generations and is listed per model. Read the number for the exact model you are calling from Google’s caching page before designing around it; a figure copied from a blog post about a previous generation is the usual cause of a cache that will not create.

Whatever the current value, the shape of the failure is the same. A create call whose content is below the floor is rejected with an HTTP 400 and an INVALID_ARGUMENT status, and the message names the required minimum and the count you supplied:

{
  "error": {
    "code": 400,
    "message": "Cached content is too small. total_token_count=1408, min_total_token_count=...",
    "status": "INVALID_ARGUMENT"
  }
}

Because the message reports both numbers, the diagnostic loop is short: run countTokens on the content you intend to cache first, compare it to the documented floor for your model, and only create the cache if it clears. Do not infer the count from file size—a PDF or a video clip tokenises nothing like its byte length.

Content below the floor is not uncacheable in every sense. The 2.5 generation applies implicit caching automatically to repeated prefixes, with its own smaller minimum and no resource to manage. Explicit caching is the one you control and the one you are billed storage for; implicit caching is the one that happens whether or not you asked.

Creating a cache and using it

The cache is a two-call pattern: create the resource, then reference it on each generation.

POST https://generativelanguage.googleapis.com/v1beta/cachedContents

{
  "model": "models/gemini-2.5-flash",
  "displayName": "handbook-2026-edition",
  "system_instruction": {
    "parts": [{ "text": "Answer only from the handbook provided." }]
  },
  "contents": [
    {
      "role": "user",
      "parts": [
        { "file_data": { "mime_type": "application/pdf", "file_uri": "https://generativelanguage.googleapis.com/v1beta/files/abc123" } }
      ]
    }
  ],
  "ttl": "3600s"
}

The response carries a name like cachedContents/xyz789, plus createTime, expireTime and a usageMetadata.totalTokenCount for what was actually stored. Generation then looks like an ordinary request with one extra field and a much shorter contents:

POST .../v1beta/models/gemini-2.5-flash:generateContent

{
  "cachedContent": "cachedContents/xyz789",
  "contents": [
    { "role": "user", "parts": [{ "text": "What is the notice period for a fixed-term contract?" }] }
  ]
}

The cached tokens still appear in your usage, at a reduced rate, under a field of their own:

"usageMetadata": {
  "promptTokenCount": 41208,
  "cachedContentTokenCount": 41192,
  "candidatesTokenCount": 96,
  "totalTokenCount": 41304
}

cachedContentTokenCount is the number to watch. If it is absent or zero on a request that named a cache, the cache was not used and you are paying full prefill without knowing it.

TTL is a storage bill, not an expiry policy

ttl is a duration string in seconds—"3600s", "86400s"—and Google documents a default of one hour when you omit it. You can also set an absolute expireTime instead, and you can extend or shorten a live cache with a PATCH to its resource.

The instinct from web caching is to set a long TTL because a stale hit is cheap. That instinct is wrong here, because you are billed for storage for the whole TTL whether or not anything reads it. The economics are a straightforward comparison, and it is worth writing out with your own numbers:

Cache is worth it when

  (requests during TTL) x (tokens) x (input rate - cached input rate)
    >  (tokens) x (storage rate per hour) x (TTL in hours)

Which reduces to a break-even request count

  requests > (storage rate per hour x TTL hours) / (input rate - cached rate)

Note that the token count cancels: the decision is about how often the
cache is read during its lifetime, not about how big it is.

That cancellation is the useful result. A cache that serves two hundred requests in an hour is obviously worth it; a cache that serves three is obviously not; and the size of the document does not enter into it. Set the TTL to the length of the burst you expect, not to the length of time the content stays true, and delete the resource when the burst is over.

Listing, extending and deleting a cache

cachedContents is an ordinary resource collection, which means the operations you would expect all exist and the one people forget is the one that costs money.

GET    /v1beta/cachedContents             list every live cache
GET    /v1beta/cachedContents/xyz789     one cache's metadata
PATCH  /v1beta/cachedContents/xyz789     change ttl or expireTime
DELETE /v1beta/cachedContents/xyz789     delete it now

The list call is the one to wire into an operational check. Caches are created programmatically, they are billed for as long as they live, and nothing in an application’s normal flow reminds you that eleven of them are sitting there with twenty-four-hour TTLs from a load test last week. A periodic listing compared against what your application believes it created is the whole of cache hygiene.

What you cannot do is as important as what you can. The content of a cache is immutable: PATCH changes only the expiry. Adding a document to a cached corpus means creating a new cache with the full content and deleting the old one, and paying to compute the whole thing again. That immutability is why caching suits stable corpora—a codebase snapshot, a released handbook, a finished video—and suits a document under active revision very badly.

Two operational details round it out. A cache is scoped to the API key or project that created it, so it is not a shared resource across environments and a staging cache will not serve production traffic. And displayName is the only place to record what a cache is for; the resource name is opaque. Set it to something that identifies the content and the version, because a listing of eleven caches named null tells you nothing about which are safe to delete.

Finally, delete rather than waiting for expiry when a burst finishes early. The storage charge accrues for the TTL you set, not for the time the cache was useful, and a DELETE is the only way to stop it.

The rules that make a cache miss

  • The model name must match exactly. Caches are bound to a specific model version. Creating a cache against an alias and then calling a pinned version, or the reverse, does not match.
  • Cached content is a prefix. Everything in the cache comes before everything in the request’s contents. You cannot insert new material in the middle of cached content.
  • System instruction and tools belong in the cache. If they were part of the cached object, they must not be re-sent on the generation call; sending them again in the request is what produces confusing duplicate-instruction behaviour.
  • Expiry is silent. Once the TTL passes, the resource is gone and a request naming it fails rather than falling back to uncached generation. Handle the not-found case by recreating the cache.