All notes

Cache Keys Start with Which Responses May Be Shared

A response-sharing contract for language, identity, query parameters, invalidation, and rollback.

English edition, edited for length. Read the complete Chinese original.

/products?sort=price&utm_source=newsletter and /products?sort=price appear to be the same list, but /products?currency=USD may not be identical to the RMB page. The first step in cache design is not to increase the TTL, but rather to answer: Under what conditions can two requests receive completely identical responses?

The cache key implements this equivalence relationship. Omitting a necessary dimension can cause data to be shared with the wrong request; adding unnecessary dimensions fragments the hit rate. This article assumes that the page allows anonymous access, and personalized content is provided through separate endpoints. If the HTML contains user names, shopping carts, or access-controlled content, the anonymous caching strategy cannot be directly applied.

Separate Cache Eligibility and Key Calculation

Assume a product page supports both anonymous browsing and member discounts. Even if a member’s requested URL is identical to an anonymous request, it should not first check the public cache and only then check whether the request is authenticated. The correct order is to determine whether the request is eligible to use the cache, calculate the key, and finally look up the object. A cache hit does not re-execute the origin’s business logic, so permission checks placed only in the origin-fetch branch cannot prevent incorrect sharing.

Cache eligibility must account for the request method, identity inputs, and response attributes. Only look up explicitly supported GET requests in this cache. Before writing, verify that the response status is allowed and that the response neither sets a session nor declares private caching directives. Decide separately whether to cache redirects and error pages. Caching a temporary permission failure for five minutes, for example, can make users continue receiving errors after access is restored. Conversely, bypassing the cache for every error can create sustained origin traffic during an outage. Address that pressure with a separate protection strategy.

Do not decide eligibility mechanically from the presence or absence of a Cookie header. Some sites have analytics cookies that do not change the body; bypassing every request carrying them reduces the hit rate. However, an allowlist must be justified against the server’s actual session behavior, not guessed from cookie names. Start by bypassing all cookie-bearing requests, collect real samples, and prove one cookie category at a time does not affect the response. That is easier to audit than ignoring the entire Cookie header from the outset. Re-evaluate this evidence when adding a login component.

When you encounter Authorization or session cookies, the safest default setting is to bypass public caching, rather than storing the entire token in the key. The token may appear in debug logs and can also create a large, difficult-to-clear cache. After permission changes, old objects may even continue to expose content that was previously accessible.

If user data is indeed cached, a clear, private caching scheme with short lifecycles and permission checks should be used, rather than simply reusing public HTML. The “Saved” status next to a public article can be supplemented with a private interface; there’s no need to make the entire page a private object just for a single button.

Classify Parameters Instead of Deleting Them All

Business parameters change the result: sorting, pagination, and currency are examples. Tracking parameters serve attribution only. Unknown parameters require conservative handling. Remove only parameters confirmed not to affect content, then sort the remaining query parameters so that different parameter ordering does not create multiple keys for the same combination.

function publicCacheURL(requestURL) {
  const url = new URL(requestURL);
  for (const key of [...url.searchParams.keys()]) {
    if (key.startsWith('utm_') || key === 'fbclid') {
      url.searchParams.delete(key);
    }
  }
  url.searchParams.sort();
  return url;
}

This approach doesn’t remove all unknown parameters. If the application introduces a new parameter, view=compact, next week, it defaults to retaining the parameter, which prevents both layouts from sharing the cache. Instead, using a whitelist of business parameters can suppress cache fragmentation caused by malicious parameters, but the whitelist must be maintained in conjunction with the routing contract. This is not just an optimization that brings benefits.

Consider /products?currency=usd&sort=price&utm_source=a. The business rules specify that currency is case-insensitive, and sorting only accepts a limited set of enumerated values. If an unknown sorting value is encountered, an error is returned. It is possible to convert usd to USD before caching, but only if the input passed to the renderer is also converted in the same way. If the cache key converts an unknown value to a default value, the original system returns an error based on the unknown value, while the other system interprets the same key differently.

There are two possible implementations here. The first caches the normalized request and has the origin consume that normalized input directly. The second forwards the original request to the origin, but requires tests to prove that the origin and the normalization function interpret it consistently. The first approach makes consistency easier to maintain, but may change the URL recorded in analytics. The second preserves the original context, but requires maintaining two sets of interpretation rules. Choose based on whether the business needs the original input, not on which function looks shorter.

Adding experiment=B exposes another hidden dimension. If experiment groups change the HTML, represent them as a finite set of variants, or load experiment-specific content through a private endpoint. Low experiment traffic does not justify cross-group response sharing: it also distorts the experiment’s measurements. When assignment cookies and cache keys disagree, testers may see the correct page while real users randomly receive another group’s content.

Include Language in the Same Contract

If /article returns different languages based on Accept-Language, the URL alone does not determine the content. Vary: Accept-Language tells HTTP caches that response variants exist. However, CDN products differ in their support for Vary and custom cache keys. Seeing this header does not establish that those variants are actually isolated.

A more straightforward solution is to embed the language encoding within the path: /zh-cn/article, /en/article. The root path is solely responsible for negotiation and redirection, while the content path remains constant. This ensures that internal links, search engine canonicalization, and edge caching all use the same set of identifiers. Furthermore, a default language setting should be implemented, and not all browser language lists should be treated as new variations.

If rendering must occur on the same path, first normalize the input into a finite set, and then use a caching mechanism that is explicitly supported by the platform. Do not directly concatenate the original request headers, as this will result in zh-CN,en;q=0.9 treating inputs that differ slightly in case or order as separate objects.

Setting Independent Resource Budgets for Parameter Explosion

Attackers do not need to access private data and can create a large number of misses by continuously adding different parameters. If the key retains all unknown parameters, every request will return to the origin; if the key ignores all unknown parameters, it may allow changes to the content parameters to be mixed with normal pages. The solution is not to choose one of these extremes, but to define limited inputs based on the route: validate and normalize known business parameters, remove known tracking parameters, and explicitly reject, ignore, or bypass unknown parameters in the cache.

Rejecting unknown parameters suits resources with strict API contracts, but may break third-party marketing links. Ignoring a parameter is appropriate only where it has been proved not to affect rendering. Bypassing the cache preserves the origin’s handling semantics, but still requires rate limiting. Document the reason for each choice and make the origin and edge follow the same rules. Cache-key design cannot replace rate limiting, because even valid parameters can have a very large number of combinations.

Estimate scale from the actual dimensions: multiply the number of languages, currencies, sort options, and pages for a rough upper bound. Then consider popularity and the long-tail distribution to decide which combinations deserve caching. Not every shareable object is worth storing. For arbitrary search terms, a short TTL or caching only frequent results may be more appropriate than unconditional writes. Measure the proportion of objects never read after insertion to see whether the cache mainly stores one-time requests.

This budget also helps explain performance regressions. The increase in returning to the original source after publishing new filtering options may not be due to a faulty caching system, but rather to an increase in the number of valid variations. By observing the number of business variations alongside the key rule versions, it’s possible to distinguish between changes in requirements and implementation errors. When dealing with inputs that cannot be constrained, it’s important to first protect the original site and the budget, and then discuss pre-warming.

Key Version Simultaneously Affects Cleanup and Rollback

When deploying the second version of the key rules, adding a version number to the internal keys can prevent the use of old and new objects together. However, this does not mean that old objects are automatically removed. Old versions may still occupy storage and can be re-hit after a rollback. It is important to record the version, content source, and expiration date, and to clearly define whether the rollback restores the old rules or continues to use the new rules to read the old content. The version number should not be changed unconditionally with each deployment, as this would clear the cache every time, making it difficult to compare performance metrics.

The cleanup process must also locate cached objects from a business resource. A product can have variants for language, currency, and device layout. Purging only the current browser URL may not cover them all. Use resource tags where the platform supports tag-based purging; otherwise, enumerate a bounded variant set or shorten the TTL. Cleanup capabilities belong in key design, not as a discovery during the first emergency content withdrawal.

When recording diagnostic information, use non-sensitive rule versions and variant tags, and do not output complete authentication headers, session values, or user-specific keys. Maintain aggregate counts for high-cardinality parameters, and set a sampling limit; otherwise, caching issues may become log costs. Hit rates should be calculated separately for each route and caching eligibility. Improvements in anonymous, cacheable requests should not be diluted by the natural miss rates of private interfaces.

Finally, perform a series of tests that are sensitive to order: first, anonymous then login, then login then anonymous; first, Chinese Yuan then US Dollars, then reverse; first, valid parameters then invalid parameters, then reverse. Requesting each input type individually can only prove that the original site returns the correct result. Only by reversing the order can we determine whether a cached object has been incorrectly reused. If cross-identity or cross-permission usage occurs, the release should be blocked, and the average hit rate should not be used to mitigate this risk.

Reducing the TTL from one hour to one minute only shortens the duration of incorrect sharing; it does not make that sharing safe. Isolate access-controlled content before reading it rather than relying on rapid expiration. Brief staleness may be acceptable for public product descriptions. Inventory and transaction quotes need separate trust contracts for the displayed page and the submission endpoint: a cached price is not the final authority for a transaction.

If using stale content for refreshing, it’s essential to record the source time of the old object and clearly define how long the object can be used after a refresh failure. Continuously using stale objects can turn temporary glitches into long-term errors. Refresh requests also need to use the same eligibility criteria as the initial write, especially for background refreshes, which should not carry a user’s session to update public objects. Concurrent refreshes can reduce redundant requests to the origin, but failure to coordinate should not violate isolation rules.

Cache validation therefore answers two independent questions: does this object belong to the current request, and is its age still acceptable? Prove the former first, then test expiration, purging, and refresh. Separating these concerns helps distinguish a key-design error from an incomplete purge or a stale-content policy exceeding the business’s tolerance.

Deriving a Withdrawal Plan from a Resource Variant Table

Imagine that a product description needs to retract a false security claim. The editing system only knows the product ID, while the edge system stores two languages, three currencies, and four sorting options. First, define the relationship between resources and objects: product details can be cleared by product ID, but the list cache contains multiple products and cannot assume that a single tag is sufficient to express two types of dependencies. After deleting the details, the list summary and search results may still display the old sentence. The problem here is dependency propagation, not shortening the lifespan of a page.

You can record resource dependencies in the publishing process:

  • Details correspond to this resource, list corresponds to categories and pagination, search summary corresponds to the search index version.
  • The list does not need to be precise to all actual keys written, but it must be able to provide a limited scope for cleanup.
  • The wider the cleanup granularity, the easier it is to ensure security, but the more frequent the source requests.
  • Finer granularity reduces the loss of cache hits, but increases the risk of missing dependencies.
  • During an incident, first make the erroneous content inaccessible, then restore the hit rate.
  • Do not use a successful cleanup request to prove that all derived content has been removed.

A release rollback also needs an explicit decision about rolling back content and key rules together. Suppose the new release changes currency display while the old renderer still implements the original rules. Rolling back only the application, without changing key naming, can let the old renderer read a new page. Rolling back the key version too can reactivate old cached objects containing the withdrawn claim. Record a triple in the release artifact: content version, renderer version, and key-rule version. This tells the on-call engineer which cache namespace to disable instead of leaving them to guess what “the previous version” means.

During the rollback exercise, first, introduce multiple variations into the test environment, then modify the source content, and execute the normal cleanup process. Verify that the markers in the main content have disappeared by reading from the details, list, and search entry. Then, simulate a failed cleanup scenario to confirm that the process does not record overall success and that it can be retried from the failure point. The platform cleanup is a distributed operation, and the propagation delay should be defined by the actual platform capabilities and observations during the exercise. We do not guarantee instantaneous global effect without such guarantees.

Make the Edge and Origin Use the Same Eligibility Evidence

Consider a subtle failure: the edge assumes an analytics cookie does not affect the body and therefore reads a public object. New origin middleware, however, uses a region field in that cookie to display tax information. Neither the edge code nor the key changed, but the conditions permitting response sharing are no longer true. Unit tests of the cache function alone will not detect the new origin dependency. Review which inputs rendering reads, especially cookies, region headers, device detection, and experiment assignments.

An eligibility record can list each input, its valid values, whether it changes the response, where normalization happens, and verification samples. It is an auditable constraint on the current version, not a proof about all future code. If device type affects only CSS, not server-rendered HTML, it can stay out of the HTML cache key. If mobile rendering starts omitting a body module, add a finite device variant or stop sharing that route. Keep the record near the route so maintainers encounter it when requirements change.

Response headers also need end-to-end checks. The origin may declare a response private, but application code can accidentally remove that policy while reconstructing the response, or custom storage logic can fail to honor it. That is an application bug, not evidence that Cloudflare’s Cache API ignores restrictions such as private or no-store by default; check the platform’s current documented behavior. Conversely, public headers generated at the edge must not make a diagnostic error page containing personal information inherit a successful response’s eligibility. Distinguish platform-managed caching from application-controlled caching and check each mechanism’s handling of response headers, status codes, and cookies. When both operate, identify which layer supplied a hit.

Verification can be performed using a dedicated test source server: for each input dimension, output a recognizable but non-sensitive plaintext marker, while setting the response headers according to the actual strategy. By sending these sample requests through the edge, check which inputs change the object, and which responses are never written. Do not use real tokens as test markers, and do not permanently expose the test diagnostic headers to all users. The eligibility test should fail when a new rendering dependency is added, not just when the cache function is modified.

Distinguish Origin Load from Cache-Layer Queuing

A single cache miss may trigger a request to the origin server, but it doesn’t necessarily mean that only one request is made. When the same frequently accessed objects expire simultaneously, multiple edge locations may each request the origin, and competition may also arise within a single node. When estimating the load, it’s important to consider the expiration time, the rate of frequent requests, and the cold start time after deployment, all on the same timeline. If the average number of requests to the origin is low, it doesn’t necessarily mean that the origin server’s capacity will not be exceeded at any given minute.

If request merging is introduced, the scope of coordination should be clearly stated. A single process’s Promise can only coordinate that specific instance, and cannot be used to claim that the source is only accessed once globally. Cross-node coordination will increase waiting times, failure recovery, and dependency on coordination services. The waiting process needs a timeout, and after the first request fails, the state must be released. The failed Promise should not permanently occupy the key. When the source server returns a private response, the coordination result cannot be directly broadcast to requests that originally did not have the same eligibility.

Preheating should also focus on explicitly high-frequency, publicly accessible objects, and control concurrency. Fetching all languages, parameters, and pagination at once will turn caching optimization into a single, intense event. It’s best to start with a small number of entry points, observe the latency and errors on the source server, and then gradually expand. If errors occur, stop expanding instead of continuing to retry and fill the cache. The results of preheating only indicate that the objects were successfully generated at that time, and do not replace subsequent cleanup and verification.

Validate with Collision Testing, Not Just Hit Detection

Prepare a test matrix: Identical sorting with different tracking parameters should hit the same object; different currencies must be different; anonymous and logged-in states cannot be shared; different language paths must be independent. For testing responses, include a generation identifier that only appears in the test environment. Also, make consecutive requests and compare the response body and cached status.

Also, test for repeated values of parameters: ?tag=a&tag=b. Whether this is equivalent to the reverse order, etc., is determined by business semantics. URLSearchParams.sort() will not define the array order for you. Test for differences in encoding, null values, and case sensitivity to avoid prematurely merging inputs that the database considers different.

After deploying a cache-key rule, observe the object count and origin requests, not just the hit-rate percentage. An explosion of unusual parameters calls for rate limiting or normalization. Incorrect response sharing calls for immediately disabling the affected cache and purging its objects. A temporary drop in hit rate is preferable to treating cross-user data leakage as an acceptable performance cost.

References: HTTP Vary, Cloudflare Cache Key.