Skip to main content

Command Palette

Search for a command to run...

Cloudflare KV Reference Sheet

Updated
5 min read
Cloudflare KV Reference Sheet
T

Just a guy who loves to write code and watch anime.

Quick Mental Model

  • KV Namespace = The actual database/storage (has ID like abc123def456)

  • Binding = Variable name in your code (like TODO, USERS)

  • Key-Value Store = Simple hash map, not a relational database

  • Eventually Consistent = Changes take time to propagate globally (up to 60 seconds)

  • Optimized for reads = Great for caching, not for frequent writes

Configuration

wrangler.jsonc

{
  "name": "my-worker",
  "kv_namespaces": [
    {
      "binding": "TODO", // Variable name in code
      "id": "06779da6940b431db6e566b4846d64db", // Production namespace
      "preview_id": "06779da6940b431db6e566b484a6a7" // Dev namespace
    }
  ]
}

Development Commands

wrangler dev                 # Local fake KV (starts empty)
wrangler dev --remote        # Uses preview_id (real KV, dev data)
wrangler deploy              # Uses id (production data)

Core Operations

Basic String Operations

// Write
await env.TODO.put("task:123", "Buy groceries");

// Read
let value = await env.TODO.get("task:123"); // Returns string or null

// Delete
await env.TODO.delete("task:123"); // No error if key doesn't exist

JSON Operations

// Store JSON (manual)
const task = { id: 123, title: "Buy groceries", done: false };
await env.TODO.put("task:123", JSON.stringify(task));

// Read JSON (manual)
let taskJson = await env.TODO.get("task:123");
const task = taskJson ? JSON.parse(taskJson) : null;

// Read JSON (automatic parsing)
const task = await env.TODO.get("task:123", "json"); // Returns object or null

// Update JSON (MUST put back!)
const task = await env.TODO.get("task:123", "json");
if (task) {
  task.done = true;
  await env.TODO.put("task:123", JSON.stringify(task)); // Required!
}

Expiration

Time-based Expiration

// Expire in X seconds
await env.TODO.put("session:abc", sessionData, {
  expirationTtl: 3600, // 1 hour
});

// Expire at specific time
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
await env.TODO.put("daily-stats", statsData, {
  expiration: Math.floor(midnight.getTime() / 1000), // Unix timestamp
});

Common TTL Values

  • 60 = 1 minute

  • 300 = 5 minutes

  • 3600 = 1 hour

  • 86400 = 24 hours

  • 604800 = 1 week

Metadata

Store with Metadata

await env.TODO.put("user:123", userData, {
  metadata: {
    version: 1,
    lastUpdated: Date.now(),
    source: "api",
  },
  expirationTtl: 3600,
});

Read with Metadata

const result = await env.TODO.getWithMetadata("user:123", "json");

if (result.value !== null) {
  const user = result.value; // Your data
  const meta = result.metadata; // Your metadata object
}

Key Naming Patterns

Good Patterns

// Hierarchical with colons
"user:123:profile";
"user:123:settings";
"user:123:sessions:abc";

// Cache keys
"cache:weather:london:2025-08-09";
"api:github:users:123";

// Feature flags
"feature:new-ui:production";
"feature:beta-test:enabled";

// Rate limiting
"ratelimit:192.168.1.1:2025-08-09-14"; // IP + hour

Avoid These

"user 123 profile"; // Spaces
"user@123#profile"; // Special characters
"john_profile"; // No structure
"userprofilejohn"; // Hard to read

Listing Keys

Basic List

const result = await env.TODO.list();
// Returns: { keys: [...], truncated: boolean, cursor: string }

List with Prefix

const userKeys = await env.TODO.list({ prefix: "user:123:" });
// Only returns keys starting with "user:123:"

Pagination

let allKeys = [];
let cursor = undefined;

do {
  const result = await env.TODO.list({
    prefix: "user:",
    limit: 100,
    cursor: cursor,
  });

  allKeys.push(...result.keys);
  cursor = result.cursor;
} while (result.truncated);

⚠️ Warning: list() only returns key names and metadata, NOT values. Still need get() for actual data.

Error Handling

Safe JSON Reading

try {
  const data = await env.TODO.get("user:123", "json");
  if (data === null) {
    return new Response("Not found", { status: 404 });
  }
  // Use data...
} catch (error) {
  if (error.message.includes("JSON")) {
    // Corrupted JSON data
    await env.TODO.delete("user:123"); // Clean up bad data
    return new Response("Data corrupted", { status: 422 });
  }
  // Network or other error
  return new Response("Service unavailable", { status: 503 });
}

Defensive Patterns

// Always provide defaults
const preferences = (await env.TODO.get("user:123:prefs", "json")) || {
  theme: "light",
  notifications: true,
};

// Safe property access
const theme = (preferences && preferences.theme) || "light";

Common Use Cases

Caching API Responses

const cacheKey = `api:weather:${city}`;
let weather = await env.TODO.get(cacheKey, "json");

if (!weather) {
  const response = await fetch(`https://api.weather.com/${city}`);
  weather = await response.json();

  // Cache for 5 minutes
  await env.TODO.put(cacheKey, JSON.stringify(weather), {
    expirationTtl: 300,
  });
}

return new Response(JSON.stringify(weather));

Session Management

// Create session
await env.TODO.put(
  `session:${sessionId}`,
  JSON.stringify({
    userId: 123,
    loginTime: Date.now(),
  }),
  { expirationTtl: 3600 }
); // 1 hour

// Check session
const session = await env.TODO.get(`session:${sessionId}`, "json");
if (!session) {
  return new Response("Session expired", { status: 401 });
}

Feature Flags

// Set flag
await env.TODO.put("feature:new-ui:enabled", "true");

// Check flag
const enabled = (await env.TODO.get("feature:new-ui:enabled")) === "true";
if (enabled) {
  // Show new UI
}

Rate Limiting

const minute = Math.floor(Date.now() / 60000);
const rateLimitKey = `ratelimit:${ip}:${minute}`;

let requests = await env.TODO.get(rateLimitKey);
requests = requests ? parseInt(requests) + 1 : 1;

if (requests > 100) {
  return new Response("Rate limited", { status: 429 });
}

await env.TODO.put(rateLimitKey, requests.toString(), {
  expirationTtl: 60, // Expire after 1 minute
});

Limits and Gotchas

Size Limits

  • Key name: 512 bytes max

  • Value size: 25MB max per value

  • Metadata: No specific limit (but keep reasonable)

Performance Notes

  • Reads: Very fast (especially after first access)

  • Writes: Eventually consistent (up to 60 seconds globally)

  • List operations: Slow and expensive, avoid in hot paths

Eventually Consistent Behavior

  • Changes visible immediately at write location

  • May take 60+ seconds to appear in other regions

  • Negative lookups (key doesn't exist) are also cached

  • If you create a key after checking it doesn't exist, other regions might still return null

When NOT to Use KV

Avoid KV for:

  • Frequent updates to same key (>10/second)

  • Transactions or atomic operations

  • Complex queries or relationships

  • Real-time consistency requirements

  • Large datasets you need to iterate through

Perfect for:

  • Caching API responses

  • User sessions and preferences

  • Configuration and feature flags

  • Static asset serving

  • Rate limiting counters

  • Read-heavy workloads