Cloudflare KV is a globally distributed key-value store for Workers and Pages — env.KV_NAMESPACE.get(key) reads a value, env.KV_NAMESPACE.put(key, value, { expirationTtl: 3600 }) writes with TTL. Values are strings by default; { type: "json" } on get auto-parses JSON. list({ prefix, limit, cursor }) paginates through keys. KV is eventually consistent — reads may be stale by up to 60 seconds globally. Bindings are declared in wrangler.toml and are available on the env object in Worker handlers. In Hono, c.env.KV accesses the binding. In Next.js on Cloudflare, getRequestContext().env.KV retrieves it inside Server Components and Route Handlers via @cloudflare/next-on-pages. Claude Code generates KV binding configs, JSON storage helpers, TTL-based caching wrappers, rate limiting patterns, and feature flag storage for Cloudflare Workers.
CLAUDE.md for Cloudflare KV
## Cloudflare KV Stack
- Version: wrangler >= 3.60, @cloudflare/workers-types >= 4.20
- Binding: [[kv_namespaces]] name = "KV" id = "..." in wrangler.toml
- Read: const val = await env.KV.get("key") — null if missing
- Write: await env.KV.put("key", JSON.stringify(data), { expirationTtl: 3600 })
- JSON: await env.KV.get("key", { type: "json" }) — parse automatically
- List: await env.KV.list({ prefix: "user:", limit: 100, cursor })
- Delete: await env.KV.delete("key")
- Next.js: import { getRequestContext } from "@cloudflare/next-on-pages"
- Consistency: eventually consistent ~60s — not suitable for counters (use Durable Objects)
Wrangler Configuration
# wrangler.toml — KV namespace bindings
name = "my-worker"
main = "src/worker.ts"
compatibility_date = "2024-01-01"
# Production KV namespace
[[kv_namespaces]]
binding = "KV"
id = "abc123..."
# Preview KV namespace for local dev
[[kv_namespaces]]
binding = "KV"
id = "def456..."
preview_id = "def456..."
# TypeScript type additions — src/worker-env.d.ts
# interface CloudflareEnv {
# KV: KVNamespace
# }
KV Storage Helpers
// lib/kv-storage.ts — typed KV utilities
import type { KVNamespace } from "@cloudflare/workers-types"
// Generic typed get/put helpers
export async function kvGet<T>(kv: KVNamespace, key: string): Promise<T | null> {
return kv.get<T>(key, { type: "json" })
}
export async function kvPut<T>(
kv: KVNamespace,
key: string,
value: T,
options: { ttl?: number; expirationDate?: Date } = {}
): Promise<void> {
const putOptions: KVNamespacePutOptions = {}
if (options.ttl) putOptions.expirationTtl = options.ttl
if (options.expirationDate) putOptions.expiration = Math.floor(options.expirationDate.getTime() / 1000)
await kv.put(key, JSON.stringify(value), putOptions)
}
export async function kvDelete(kv: KVNamespace, key: string): Promise<void> {
await kv.delete(key)
}
// List all keys with a prefix (handles pagination)
export async function kvListAll(
kv: KVNamespace,
prefix: string,
limit = 1000
): Promise<string[]> {
const keys: string[] = []
let cursor: string | undefined
while (true) {
const result = await kv.list({ prefix, limit: Math.min(limit - keys.length, 1000), cursor })
keys.push(...result.keys.map(k => k.name))
if (result.list_complete || keys.length >= limit) break
cursor = result.cursor
}
return keys
}
// Cache wrapper — read from KV, fallback to fetch, store result
export async function withCache<T>(
kv: KVNamespace,
cacheKey: string,
fetcher: () => Promise<T>,
ttlSeconds = 300
): Promise<T> {
const cached = await kvGet<T>(kv, cacheKey)
if (cached !== null) return cached
const fresh = await fetcher()
await kvPut(kv, cacheKey, fresh, { ttl: ttlSeconds })
return fresh
}
Hono Worker with KV
// src/worker.ts — Hono app with KV bindings
import { Hono } from "hono"
import { cache } from "hono/cache"
import { kvGet, kvPut, withCache } from "./lib/kv-storage"
interface Env {
KV: KVNamespace
}
const app = new Hono<{ Bindings: Env }>()
// Feature flags stored in KV
interface FeatureFlags {
newCheckout: boolean
experimentalSearch: boolean
maintenanceMode: boolean
}
const DEFAULT_FLAGS: FeatureFlags = {
newCheckout: false,
experimentalSearch: false,
maintenanceMode: false,
}
app.get("/api/flags", async c => {
const flags = await withCache<FeatureFlags>(
c.env.KV,
"feature-flags",
async () => {
const stored = await kvGet<FeatureFlags>(c.env.KV, "feature-flags")
return stored ?? DEFAULT_FLAGS
},
60 // Cache for 60s — flags change infrequently
)
return c.json(flags)
})
app.put("/api/flags", async c => {
const updates = await c.req.json<Partial<FeatureFlags>>()
const current = await kvGet<FeatureFlags>(c.env.KV, "feature-flags") ?? DEFAULT_FLAGS
const updated = { ...current, ...updates }
await kvPut(c.env.KV, "feature-flags", updated)
// Invalidate cache
await c.env.KV.delete("feature-flags:cache")
return c.json(updated)
})
// Session storage in KV
interface Session {
userId: string
role: string
createdAt: string
}
app.get("/api/session/:token", async c => {
const session = await kvGet<Session>(c.env.KV, `session:${c.req.param("token")}`)
if (!session) return c.json({ error: "Session not found" }, 404)
return c.json(session)
})
app.post("/api/session", async c => {
const body = await c.req.json<{ userId: string; role: string }>()
const token = crypto.randomUUID()
const session: Session = {
userId: body.userId,
role: body.role,
createdAt: new Date().toISOString(),
}
// Sessions expire after 7 days
await kvPut(c.env.KV, `session:${token}`, session, { ttl: 7 * 24 * 3600 })
return c.json({ token })
})
export default app
Rate Limiting with KV
// lib/rate-limiter.ts — sliding window rate limiting
import type { KVNamespace } from "@cloudflare/workers-types"
interface RateLimitResult {
allowed: boolean
remaining: number
resetAt: number
}
// Simple fixed-window rate limiter using KV
// Note: KV is eventually consistent — use Durable Objects for strict rate limiting
export async function checkRateLimit(
kv: KVNamespace,
identifier: string, // e.g., IP address or user ID
{
limit = 100,
windowSeconds = 60,
}: { limit?: number; windowSeconds?: number } = {}
): Promise<RateLimitResult> {
const windowStart = Math.floor(Date.now() / (windowSeconds * 1000))
const key = `rate:${identifier}:${windowStart}`
const current = await kv.get(key) ?? "0"
const count = parseInt(current, 10)
if (count >= limit) {
return {
allowed: false,
remaining: 0,
resetAt: (windowStart + 1) * windowSeconds * 1000,
}
}
// Increment counter
await kv.put(key, String(count + 1), { expirationTtl: windowSeconds * 2 })
return {
allowed: true,
remaining: limit - count - 1,
resetAt: (windowStart + 1) * windowSeconds * 1000,
}
}
Next.js on Cloudflare Pages
// app/api/flags/route.ts — KV in Next.js Route Handler
import { getRequestContext } from "@cloudflare/next-on-pages"
export const runtime = "edge"
export async function GET() {
const { env } = getRequestContext()
const flags = await env.KV.get("feature-flags", { type: "json" })
return Response.json(flags ?? {})
}
// middleware.ts — KV access in Edge Middleware
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { getRequestContext } from "@cloudflare/next-on-pages"
export const runtime = "experimental-edge"
export async function middleware(request: NextRequest) {
const { env } = getRequestContext()
// Check maintenance mode from KV
const maintenance = await env.KV.get("maintenance-mode")
if (maintenance === "true" && !request.nextUrl.pathname.startsWith("/maintenance")) {
return NextResponse.redirect(new URL("/maintenance", request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
}
For the Cloudflare Durable Objects alternative when strongly consistent counters, atomic operations, or WebSocket session state persisted at the edge are needed — Durable Objects guarantee single-writer consistency that KV cannot provide, essential for rate limiting, reservation systems, and real-time collaboration state, see the Durable Objects guide. For the Redis/Upstash alternative when rich data structures (sorted sets, streams, pub/sub), atomic Lua scripts, or compatibility with existing Redis tooling are needed from a serverless-compatible store — Upstash provides Redis API compatibility over HTTP for edge environments, see the serverless Redis guide. The Claude Skills 360 bundle includes Cloudflare KV skill sets covering caching, session storage, and feature flags. Start with the free tier to try edge storage generation.