Caching Headless API Responses with Cache Tagsfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Trimming a response's size (the last lesson) only helps if you're actually generating that response fresh every single request. Cache tags are how you avoid that entirely — serve the exact same cached JSON response to a thousand API clients, and invalidate it precisely the moment the underlying content actually changes, not a second sooner or later.

What you'll learn in this lesson

  • Why cache tags matter even more for a headless API than for a themed page
  • How Drupal automatically attaches cache tags to JSON:API responses
  • How to read the X-Drupal-Cache-Tags header to verify what's actually being tracked
  • How your reverse proxy (Varnish, CDN) should respect these same tags for automatic invalidation

Why cache tags matter more for headless APIs

On a themed Drupal page, a cache miss costs one visitor a slightly slower page load. On a headless API serving a mobile app or a high-traffic frontend, a cache miss on a popular endpoint can mean rebuilding the same JSON payload hundreds of times per second under load. Cache tags are Drupal's mechanism for tracking exactly which cached items depend on which pieces of content, so a single node edit invalidates only the responses that actually included that node — not your entire API cache.

JSON:API's automatic cache tags

You don't have to do anything special to get cache tags on JSON:API responses — the module attaches them automatically, following the same rules as Drupal's render pipeline:

GET /jsonapi/node/article/abc-123-uuid

That single-node response carries a tag like node:42. A collection response (/jsonapi/node/article) carries a tag per node in the list plus a list-level tag (node_list), so creating a brand-new article correctly invalidates the collection view even though the new node obviously couldn't have been tagged on it yet.

Verifying it: the X-Drupal-Cache-Tags header

Add the internal Dynamic Page Cache or an external reverse proxy in front of your site, and every cacheable response exposes exactly what it's tagged with:

curl -sI https://example.com/jsonapi/node/article/abc-123-uuid | grep -i x-drupal-cache-tags

You'll see something like node:42 node_list config:filter.format.basic_html user:1 — every entity, config object, and even the rendering user account that this specific response's correctness depends on. If a tag you expected isn't there, that's a real signal something in your stack (a custom formatter, a computed field) isn't reporting its cache dependencies correctly.

Cache tags only help if something downstream reads them. Drupal's own internal Dynamic Page Cache respects them out of the box. Varnish and most CDNs need to be explicitly configured to purge by tag (via a header-based purge mechanism, since Varnish itself has no native concept of Drupal cache tags) — skip that step and you'll have correct tags with nothing actually acting on them, which looks identical to having no caching at all until you go looking.

Quick check: an editor updates one article's title. Should that invalidate just that article's JSON:API response, or every article's response? (Just that one — because node:42 only appears on responses that actually included node 42. The collection endpoint that lists articles also gets invalidated, since it's tagged node_list, but a completely unrelated article's individual endpoint is untouched.)

Key takeaways

  • JSON:API responses carry cache tags automatically — no custom code required to opt in.
  • A single-entity response is tagged with that entity; a collection response adds a list-level tag so newly-created items still correctly invalidate it.
  • Inspect the X-Drupal-Cache-Tags response header to verify exactly what a given response depends on.
  • A CDN or Varnish layer must be explicitly configured to purge by tag — Drupal generating correct tags doesn't automatically mean your reverse proxy is acting on them.

Coming up next

Caching correctly-tagged individual responses is one win. The next lesson tackles a related but distinct problem: cutting the number of requests your frontend makes in the first place, by batching related data into fewer round trips.