await caches.default.put(request, response): Successful execution does not mean that every edge node globally has this response. The object scope of the Cloudflare Workers Cache API is different from the CDN caching mechanism used with fetch(). Ignoring this difference can lead developers to misdiagnose cache misses across regions as a TTL failure.
This document discusses publicly accessible and shareable responses that do not involve account data. Specific caching eligibility, billing, and product capabilities may vary depending on the configuration. Therefore, it is important to consult the current account documentation and not assume that any response can be cached.
Clearly Define the Cache Layers You Control
After a request enters the Worker, you can call fetch() upstream and rely on the platform’s caching rules, or query the Cache API explicitly and decide when to write. The latter gives the application more direct control, but also makes it responsible for keys, expiration, failures, and invalidation.
if (request.method !== 'GET'
|| request.headers.has('Authorization')
|| request.headers.has('Cookie')) {
return fetch(request);
}
const cache = caches.default;
const key = new Request(new URL(request.url), { method: 'GET' });
const cached = await cache.match(key);
if (cached) return cached;
const response = await fetch(request);
if (response.ok && !response.headers.has('Set-Cookie')) {
const stored = new Response(response.body, response);
stored.headers.set('Cache-Control', 'public, max-age=60');
ctx.waitUntil(cache.put(key, stored.clone()));
return stored;
}
return response;
This is a semantic example, not a generic middleware that can be directly reused. A production implementation must also exclude information about identities, Set-Cookie, content that is not suitable for caching, and situations where requests are affected by conditions. The example will also mark the response as publicly cacheable. If the browser should not cache the same content for the same time, separate caching and storage policies should be set for the response and the storage.
Assume a user requests a Worker. The Worker queries the Cache API, but the cache miss occurs. The Worker then uses fetch to access the original site. This fetch request also passes through the platform’s CDN. At this point, there are four potential sources: client-side cache, Worker’s manual cache, the upstream CDN, and the original site. A cache miss by the Worker does not necessarily mean that the original site was requested; a hit by the upstream CDN does not necessarily mean that the manual cache already exists. If the logs only record a single “cache status,” the same response time variation could be interpreted as having different causes.
Define independent fields for each layer: manual query results, upstream status, time to source, and rule version. Do not put all of this information into public response headers; only record secure aggregations when dealing with internal addresses or identities. Debug headers can be enabled in a controlled environment and their meaning should be clearly defined. For example, the “manual hit” field only describes the code branch, not whether the object was just populated from another cache layer. Diagnostic information must be able to answer questions, rather than creating a seemingly authoritative “HIT” tag.
Before choosing a caching method, it’s important to consider why the application needs to write its own data. If the caching is simply for publicly available static HTML, the platform’s standard caching rules may already be sufficient. If the response is generated from multiple data sources, with keys being normalized according to business rules, and only limited calculation results need to be cached, a manual API might offer clear value. Adding a caching layer will increase the costs associated with invalidation and observation, and it shouldn’t be added simply because the API is simple.
Manual caching requires a clear state transition table.
A request can be in one of these states: ineligible, cache hit, cache miss, cacheable origin response, uncacheable origin response, or origin failure. Define the return value for each state instead of putting every branch inside one try/catch. An ineligible request follows normal business logic; a hit returns the cached object; a miss goes to origin; a cacheable response is stored in the background; an uncacheable response is returned without storage; and an origin failure follows the application’s error policy. Cache exceptions and origin exceptions must not collapse into the same fallback page.
For example, if cache.match throws, the Worker should continue to the origin and log the cache failure. If fetch throws, however, it must not fabricate a normal 200 response. If the application supports stale-content fallback, it needs a deliberately retained version with a defined validity window; it must not return an arbitrary old object after catching an exception. Cache API expiration and an application-managed stale copy are different mechanisms. Confirm the API’s actual behavior before implementation instead of assuming that expired objects remain readable.
The results of the write operation cannot be used to determine whether the current response is considered a “successful publication.” Publication should be determined by deployment and public record, with caching serving only to accelerate subsequent operations. If the logs treat cache.put as a successful publication of an article, it will result in multiple counts in multiple regions. Separating business events from performance events avoids incorrect metrics and reduces the coupling between caching operations and the content system.
The capacity budget can be assessed based on the size of the object, the number of variations, and the hit reuse rate. Any query parameters will create temporary objects, and very large documents will increase the cost of stream replication. Neither of these issues can be solved by simply extending the TTL. First, limit the business input and cache types, and then determine the expiration time. By default, unknown content types are not written, and new types must be individually validated for their headers, size, and expiration methods.
Testing in the development environment also has limitations in scope. Local emulators can verify code branch and commit handling, but they may not necessarily replicate a global network, real-world cleanup capabilities, or edge locations. The report should clearly state which tests were performed locally and which were performed in a deployment preview environment. Do not treat local hits as definitive evidence of global production semantics, and do not expose all user data caches for real testing. Using synthetic, publicly available resources is sufficient to verify the mechanism, and there is no need to risk collecting private responses.
Cloning and stream consumption are not mere formalities
The response body is a stream. Directly using the same response for both writing to the cache and returning to the client may lead to errors caused by consuming the response body. Use clone() to create an independent read branch, but also be mindful of the resource consumption of large responses. Avoid unconditionally converting all files to strings for convenience before copying.
ctx.waitUntil() allows a cache write to continue after the response is returned, but does not guarantee that the write succeeds. Cache errors should be observable without disrupting a successful origin response. For the application, caching is an acceleration layer and must not be the sole condition for a public page’s availability.
By placing cache.put within the waitUntil block, the user will initially receive a normal response, while the write operation continues in the background during the allowed lifecycle of the request. If the write fails, the current user is not necessarily affected, but the next request may still retrieve from the cache. Production code should capture and log write errors to avoid unhandled rejections. Additionally, limit the logging frequency to prevent the same issue from generating thousands of errors per second.
Requesting immediately after the initial response, especially if it’s likely to complete before the first request finishes, can lead to unstable results. If the assumption “completion of the first request equals cache completion” is used, the results will be unreliable. To ensure accurate testing, it’s possible to expose a controlled completion marker or use a bounded polling mechanism to confirm the write; however, relying on a fixed wait time (e.g., a few milliseconds) as a guarantee is not reliable. The test report should clearly state whether the validation confirms the final write or immediate consistency.
Cache misses can also lead to multiple requests hitting the origin server simultaneously. The Cache API itself is not a universal lock, and local memory variables should not be used as a global request coordination mechanism. If the original site cannot handle repeated generation, a dedicated coordination scheme or platform capability is required. If the generation cost is low, allowing a small amount of repetition can be simpler. First, quantify the cost of hot requests, and then decide whether to introduce additional state such as Durable Objects to avoid increasing system complexity for non-existent bottlenecks.
Creating a new Response can adjust headers, but its status, content encoding, and body must remain consistent. If code reads the body and modifies the text, the original Content-Length and strong ETag may no longer be accurate. If it only forwards the stream, it still must not copy headers that are unsuitable for public use. Cache eligibility should be based on the complete response, not just response.ok.
The origin might return Cache-Control: private or no-store, and the application must not overwrite that policy with public merely because the status is 200. Set-Cookie is a clear warning signal, but its absence does not prove that the body is anonymous: the upstream might personalize content from request identity without setting another cookie. Both request and response must satisfy the public-sharing policy; if either cannot prove eligibility, bypass the cache.
The large response clone also needs to consider consumer speed. When returning branches and caching writes are performed in parallel, the buffering behavior of the platform stream can affect memory. Do not include any downloaded files in the same caching middleware as small HTML files. Establish clear policies for volume, type, and range requests, and responses that cannot be cached should be streamed directly. The performance optimization boundaries should include resource limits, not just whether the code can run.
Cleaning strategies should be designed in pairs with the writing method.
Before updating the content, please first ask:
- Is the cache key versioned?
- Is it possible to clear the cache by URL?
- Where are the manual deletions applied?
- Will deploying new Workers clear out the old cache?
Do not treat “deploying new code” as the default mechanism for invalidating the cache.
For static content, using content hash resource URLs can reduce the need for proactive cleanup. For HTML, use a limited TTL and provide an emergency cleanup path. When using custom cache keys, verify that the platform’s cleanup API can accurately match them. The observability, cleanability, and hit rate of the keys are all equally important.
The acceptance criteria should include: cache write failures, upstream 500 errors, authentication requests, updates to old objects, and initial requests from different regions. Once these scenarios are documented in the tests, it will be possible to determine whether the manual Cache API is more suitable for the current needs than the platform’s standard caching mechanism.
A successful cache.delete call at a specific location only proves that the call can affect the corresponding cache environment. It does not, however, prove that all regions have been cleared. It is necessary to verify the platform documentation regarding the support for custom keys in the scope and cleanup interfaces. If the cleanup relies on the original URL, while the storage uses a rewritten URL, the maintenance scripts may not be able to find the object. Recording the mapping from public resources to internal keys is part of the recovery process.
Content hashing is suitable for immutable resources: new content uses a new address, and old objects naturally lose their references. Fixed-address HTML requires explicit TTL and cleanup policies. Version keys can prevent new code from misinterpreting old formats, but they cannot prevent old deployments from reading old objects; when rolling back, you must check if the old key is still valid. Do not automatically include deployment versions in all keys, which will cause cold starts every time a new version is released.
For urgent withdrawals, it’s possible to first instruct the worker to reject the corresponding resource before checking the cache, followed by cleanup, to prevent further retrieval during the cleanup process. The rejection list itself requires a reliable source and update path, and should not only exist in a single instance’s memory. This approach is suitable for clear, urgent scenarios and should not be used to make the cache layer a daily content access control system.
Deconstructing the Timeline of a Cold Start Request via a Single Online Session
Imagine the first reader requests an article after deployment. The manual cache misses, and fetch receives an older version from the upstream CDN. The Worker then stores that response in its local cache. The second reader gets a fast manual-cache hit but still reads stale content. A cache hit is therefore not successful acceptance: the origin path does not guarantee that the content is current. Deployment must verify the complete version chain instead of adding a new Worker version label to an old upstream object.
Record timestamps for request start, manual lookup completion, upstream return, and background write completion, together with a non-sensitive public-content version. If the content is not the expected version, either skip the write or keep verifying; do not relabel an old object with the new deployment version. Separating application and content versions explains how new code can still return an old article. Logs should contain only necessary identifiers, never article bodies, authentication headers, or private data.
Once the write-back operation is complete, another request might still miss the target location. The test report should preserve the actual edge location and ensure that the client’s network address is not treated as the same cache instance. Results from different locations do not need to be forcefully synchronized, as long as they are consistent with the chosen strategy. If the product requires immediate global visibility, a suitable architecture must be chosen, rather than continuing to add fictitious global semantics to local APIs.
When concurrent writes occur at the same location, older requests will complete later than newer requests, potentially overwriting updated objects. Version keys can isolate different versions, but fixed key writes do not guarantee a global order. If data updates are frequent and order is important, it’s necessary to evaluate a reliable version source or coordination mechanism. Public static articles are often simpler to manage using immutable version resources and explicit entry updates; however, transaction states are not suitable for this caching approach as an authoritative source.
Run a failure-injection tabletop exercise for invalidation
First, list the steps required for an emergency deletion:
- Verify public resources
- Prevent further writes
- Clean up existing objects
- Check different entry points and restore normal caching.
Important notes:
- Only clean once, without blocking old write-backs. A request currently in progress may re-save the old content after cleaning.
- The effects of “prevent write” and “prevent read” should be verified based on the actual worker configuration source and deployment scope.
- Do not treat changes to in-memory variables as a distributed barrier.
The exercise can involve writing markers in a synthetic document, followed by a controlled delay in a read-back request. After the rollback, the request is executed, and the markers are checked for reappearance. This example specifically validates the competition between clearing and writing, and is closer to a real-world scenario than a simple “put then delete” operation. If the existing architecture cannot guarantee strong rollback, it is sufficient to record an acceptable propagation window or temporarily bypass the cache, without claiming that the extended exercise wait time resolves the race condition.
Successful API cleanup should also be combined with the actual object retrieval. For custom keys, cleanup based on the public URL may not hit the internal storage identity. The cleanup capabilities and pricing schemes may also differ for different caching mechanisms. The maintenance tool should display the scope of resources to be cleaned and the version of the rules, and restrict the target domain to prevent accidental clearing of other sites. The operation log should record the response results instead of tokens, and retain the version information that can be recovered.
Before restoring the cache, ensure that the original content is correct and that the eligibility checks are still valid. Directly restoring the old rules could allow objects that have not yet expired to be read again. While the new version space can isolate old objects, it also creates a cold start pressure. Pre-warming should be controlled to handle only approved public resources. Security restoration and performance restoration can be phased, first making the correct content readable, and then gradually improving the hit rate, avoiding the need to re-activate problematic objects in order to restore metrics.
Specify separate header policies for browsers and stored responses
Browser, shared proxy, and application storage are distinct entities. The example of including public, max-age in the response is a deliberate choice, not a necessary step when using the Cache API. If the client must always re-validate, while the edge server allows one-minute reuse, this should be expressed through mechanisms supported by the specific platform, and the final response received by the browser should be verified. The response headers stored within the application may not necessarily be equivalent to the standard cache headers defined by the platform, and cannot be explained simply by using a generic name.
After updating the main content, older browsers may still display cached data, even if the Worker has cleared it. This behavior is likely intended to reflect the freshness of the content. First, verify whether a network request occurred. Then, check which layer’s response object is being displayed. When testing, avoid constantly checking “Disable Cache” to prevent the inability to detect client-side validity issues. Save the results of the first visit, normal revisit, forced validation, and offline access separately, and explain how each path ensures the visibility of the updated content.
Proxies can also compress or transform the response. If the application modifies the body, the content length, validator, and encoding headers should be re-evaluated; if it’s just a streaming forward, these headers should remain consistent with the actual bytes. To facilitate manual caching of large files, reading the entire file into memory can increase memory usage and latency, and should not be used as a general approach. Using a whitelist of cache types and a size budget can help separate small calculation results from large file downloads.
A responsibility matrix should answer: Who is responsible for recovery?
When choosing a caching mechanism, the platform is responsible for enforcing the caching rules, while the application is responsible for defining eligibility criteria, trusted sources, and business failure requirements. When choosing a Cache API, the application is also responsible for querying, writing, key mapping, and handling backend errors. To ensure clarity, these responsibilities should be documented in a mechanism comparison table, and then compared to determine if additional control is truly necessary. It’s important to note that comparing only the number of lines of code is insufficient; the cost of exception handling and cleanup exercises is typically much higher.
If both mechanisms are present, record the actual layer used for each route, along with its TTL, version source, and cleanup interface. When adjusting routes, check to ensure that old layers are not left behind, to avoid mistakenly assuming that all responses are no longer cached after manually removing cached code. One mechanism’s switch does not control the other; this is the key distinction. The criteria for passing are clear responsibility and actual responses that comply with the policy, rather than simply showing “HIT” for each layer.
Cache-hit tests should cover multiple locations
Repeated successful requests from the same location can only demonstrate caching behavior on that specific path. Cross-region testing should record the edge location, time, key, and version identifier, and observe whether each location requires its own connection to the origin. Do not conclude that “global caching has been pre-warmed” based on a single global speed test.
Manually adding X-Cache: HIT can be helpful for debugging, but it only represents the state of your own code branch, not the hit status of the browser or upstream CDN. Multiple layers of caching should be identified separately, otherwise, hits from the upstream and Worker will be mixed together, making it impossible to determine the number of returns.
It’s also important to distinguish between expiration and premature expiration. The fact that an object can survive for one minute doesn’t mean that an application can treat the cache as a database that reliably stores data for at least one minute. Storage and eviction policies should be checked against the current platform contract. Testing should ensure that the backup mechanism returns a miss within the validity period, confirming that the application can still regenerate the data from a trusted source, and that the business success count is not duplicated. The existence of a cache is not a guarantee that the data is publicly available.
If the primary source is unavailable, the recovery strategy must be pre-selected during design: controlled errors, independent saving of allowed older versions, or other approved paths. Do not expand the key range or use other languages or old permission objects to fill the gaps at this time. The acceptance record, which lacks both caching and source, can distinguish between these two stages. It is more valuable than simply repeating normal hits to determine if a backup path actually exists.
References: Workers Cache API, Workers Cache Behavior.