Cloudflare
KV
Cloudflare KV
Produção
Padrões
Cache
Limites

KV in production: the patterns that work and the ones that are misleading at first

Cloudflare KV usage patterns that work in production: metadata trick, module-level caching, index key for list(), and anti-patterns that exhaust the free tier quickly.

KV in production: the patterns that work and the ones that are misleading at first

A thousand writes a day seems enough until you get anything into production with real users. A login system that writes a session token to the KV for authentication exhausts this limit with half an hour of moderate traffic. The first confrontation with the real limits of the KV rarely happens in staging.

The free tier — 100,000 reads and 1,000 writes per day — was designed to reflect the correct usage model: lots of reading, minimal writing. When a team uses KV consistently with this model, the free tier lasts a long time. When used as a session bank or per-user state store, the limit appears in the first week.

Patterns that work

The most solid use of KV is the storage of application configuration read repeatedly and changed rarely. A JSON with product flags, business parameters, whitelists, third-party endpoints — this type of data changes per administrative action, not per user. A write to the KV propagates to all PoPs and serves millions of requests at no relevant cost. A team that writes to this key ten times a month and reads it ten million times is comfortably within the free tier.

Caching of rendered HTML follows the same principle. A blog post, a product page, a search result that doesn't change per user — you render it once, save it in the KV with an appropriate TTL, and serve it straight from the PoP cache for any subsequent requests. The cost of rendering drops, latency drops, and the number of writes becomes proportional to the frequency of content updates, not the volume of traffic.

Session tokens with TTL also fit, as long as the session is write-once. You write the token upon authentication — one write per login — and read it on each subsequent request. If a user logs in once and is active for hours, the read/write ratio is excellent. What breaks this model is the session with mutable state: each session data update becomes a write, and the cost explodes.

The trick with metadata

Each key in the KV can carry up to 1024 bytes of arbitrary JSON metadata, separate from the value itself. This field is returned by getWithMetadata() along with the value, in a single operation — with no additional reading cost.

The practical use is to store information alongside the value that you would need to parse or infer in another way. For a binary file saved in KV, the metadata can contain Content-Type, ETag, creation date, original size, and any relevant HTTP headers. The Worker reads the key, receives value and metadata in one call, and assembles the HTTP response with the correct headers without any additional lookup or parse logic.

This also works for light versioning. Save the version or timestamp of the last update in the metadata. Any consumer can check whether they are reading the expected version without searching for a second piece of data.

The list() performance problem

list() is the most expensive KV operation in terms of relative performance, and it is the one that appears most frequently on paths that should not use it. A call to list() in a namespace with 100 thousand keys is slow — the latency will depend on the size of the namespace and the cursor — and counts as a list operation, which has a separate quota: 1 thousand operations per day in the free tier, $0.50 per million in the paid one.

The real problem is using list() in the hot path of a request. If each request needs to discover what keys exist to serve a response, you have placed a data management operation within the critical performance path.

The solution is to maintain an index key. You write a key like __index__ in the KV whose value is a JSON with the list of namespace keys — or just the identifiers needed for the logic. When the namespace changes, you update the index along with the main write. The cost is one extra write per write operation. The benefit is that any index read is a regular read, with cache latency and without the scaling issues of list().

This pattern has the obvious limitation that the index needs to be kept in sync manually. If you have multiple Writers, the absence of atomic operations in KV creates a window of inconsistency in the index. For namespaces with single writing or writing controlled by a single Writer, the pattern works well.

Module-level caching: the optimization that no one explicitly documents

Workers on Cloudflare run on V8 isolates. A single isolate can serve thousands of requests before being evicted. Variables declared in module scope — outside the handler — persist between requests as long as isolate is active.

This creates a simple and effective optimization opportunity for configuration data. Instead of doing env.CONFIG.get('settings') in each request, you declare a variable in the module scope and only search the KV when it has not yet been initialized:

let config = null; export default { async fetch(request, env) { config = config ?? await env.CONFIG.get('settings', { type: 'json' }); // usa config } };

The first isolate request reads the KV. All subsequent requests for the same isolate use the in-memory value. For data that rarely changes — product configuration, feature flags — this eliminates the KV reading of almost all requests, reducing latency and consumption of read operations.

The implication is that an update to the KV is not immediately reflected in all Workers — each isolate will continue to use the cached value until it is evicted. For data where 60 seconds to a few minutes of delay are acceptable, the tradeoff is excellent. For data that needs to be updated immediately across all Workers, this pattern is not suitable.

What not to take into production without rethinking

Using KV as a job queue does not work. Without atomic operations, two Workers can read the same job, process it in duplicate, and mark it as complete independently. The result is duplicate processing with no detection mechanism.

Saving mutable user data by user key scales poorly with the write limit. An application with 10 thousand active users per day that updates profile data even once per session is already in the order of magnitude of the paid limit of 1 million monthly writes — and the cost per write at $0.50/million starts to appear when you go beyond.

Namespaces with high key density and frequent need for listing are a performance trap. list() is slow in large namespaces and should not be in the request path. If your use case requires frequent listing, the data model needs to change — either with manually maintained index keys or with a different tool.

What the free tier reveals about the design

The free tier limits — 100,000 reads for 1,000 writes — are a design document in disguise. The 100:1 ratio between reads and writes is not arbitrary. It describes the workload that the KV was built to serve. Any use that inverts or approximates this ratio is outside the intended operating model, and will encounter cost, performance or consistency limitations that do not appear in low volume tests.

Also read