Cloudflare Workers KV / D1 / R2 Fix 6 min read

Cloudflare Workers Errors: CPU Limit, Memory, KV, D1 & R2 Fixes

Exact error messages, root causes, and working code fixes for the most common Cloudflare Workers runtime errors — CPU time limit exceeded, memory limit, KV null reads, D1 binding mismatches, R2 bucket access, and secrets vs plain vars.

Cloudflare live status

Cloudflare — live status

Updated every 5 minutes · Full incident history →

Full status →

Common errors and fixes

1. CPU time limit exceeded

Error: Worker exceeded CPU time limit. Free tier: 10ms CPU / 30ms wall-clock per request. Paid tier: 30 seconds CPU. CPU time counts only active JS execution — awaiting fetch/KV/D1 does not count.

# Check your plan limits
# Free:  10ms CPU time, 30ms wall-clock
# Paid:  30s CPU time (Workers Paid $5/month)
# Upgrade via: Cloudflare Dashboard -> Workers & Pages -> Plan

Move heavy work out of the request path using a Queue consumer:

// wrangler.toml — add queue binding
[[queues.producers]]
binding = "MY_QUEUE"
queue = "heavy-tasks"

[[queues.consumers]]
queue = "heavy-tasks"
max_batch_size = 10
// Worker: enqueue instead of processing inline
export default {
  async fetch(req: Request, env: Env) {
    const body = await req.json();
    // Push to queue, return immediately
    await env.MY_QUEUE.send(body);
    return Response.json({ queued: true });
  },
  // Consumer runs with a longer CPU budget
  async queue(batch: MessageBatch, env: Env) {
    for (const msg of batch.messages) {
      await doHeavyWork(msg.body);
      msg.ack();
    }
  },
};
Tip: Durable Objects also have a higher CPU budget and are suitable for long-lived WebSocket connections or stateful tasks.

2. Worker exceeded memory limit (128 MB)

Error: Worker exceeded memory limit. Workers are capped at 128 MB per isolate. The most common cause is buffering large responses or loading large files into an ArrayBuffer.

// BAD: buffers entire R2 object into memory
const obj = await env.MY_BUCKET.get('large-file.json');
const text = await obj.text(); // 200 MB file = crash

// GOOD: stream the R2 object body directly
const obj = await env.MY_BUCKET.get('large-file.json');
if (!obj) return new Response('Not found', { status: 404 });
return new Response(obj.body, {
  headers: { 'Content-Type': obj.httpMetadata?.contentType ?? 'application/octet-stream' },
});
  • Never load large ML models or embedding files in global module scope.
  • Avoid unbounded in-memory caches — use KV with expirationTtl instead.
  • Use TransformStream to process data in chunks rather than accumulating it.

3. KV "Key not found" / unexpected null

env.KV.get(key) returns null for both missing keys and keys not yet propagated (KV has eventual consistency — up to 60 seconds globally). Use getWithMetadata for conditional reads and list() to enumerate keys safely.

// Conditional check with metadata
const { value, metadata } = await env.KV.getWithMetadata('user:42', 'json');
if (value === null) {
  // Key does not exist (or hasn't propagated yet)
  return Response.json({ error: 'not found' }, { status: 404 });
}

// List keys with prefix — never assume you know all keys
const list = await env.KV.list({ prefix: 'user:' });
for (const key of list.keys) {
  console.log(key.name, key.expiration);
}
Consistency note: KV reads from the nearest edge cache. A write in one region may not be visible for up to 60 seconds from other regions. Design for stale reads or use Durable Objects for strong consistency.

4. D1 database errors — binding and safe queries

Error: Cannot read properties of undefined (reading 'prepare'). The binding name in your Worker code must match exactly what is declared in wrangler.toml. Always use .bind() — never string interpolation — to prevent SQL injection.

# wrangler.toml — binding name must match env.DB exactly
[[d1_databases]]
binding = "DB"          # <- used as env.DB in Worker code
database_name = "my-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
// BAD: string interpolation — SQL injection risk
const userId = params.get('id');
const row = await env.DB.prepare(`SELECT * FROM users WHERE id = ${userId}`).first();

// GOOD: use .bind() for parameterised queries
const row = await env.DB
  .prepare('SELECT * FROM users WHERE id = ?1')
  .bind(userId)
  .first();

if (!row) return Response.json({ error: 'not found' }, { status: 404 });

Run migrations locally with wrangler d1 migrations apply my-db --local and against production with --remote.

5. R2 bucket access — use Workers binding, not S3 SDK

Do not use the AWS S3 SDK inside a Worker — it adds unnecessary bundle weight and may hit CPU limits. Use the native R2 binding declared in wrangler.toml instead.

# wrangler.toml
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-r2-bucket"
// Put an object
await env.MY_BUCKET.put('uploads/photo.jpg', request.body, {
  httpMetadata: { contentType: 'image/jpeg' },
});

// Get an object (returns null if not found)
const obj = await env.MY_BUCKET.get('uploads/photo.jpg');
if (!obj) return new Response('Not found', { status: 404 });
return new Response(obj.body);

// Delete an object
await env.MY_BUCKET.delete('uploads/photo.jpg');
Public access: R2 objects are private by default. Enable public access per-bucket in the Cloudflare Dashboard or serve them through a Worker with access control logic.

6. Environment variables vs secrets

[vars] in wrangler.toml are plain text committed to source control. Never put API keys or passwords in [vars].

# wrangler.toml — safe for non-sensitive config only
[vars]
ENVIRONMENT = "production"
LOG_LEVEL = "info"

# DO NOT put secrets here:
# API_KEY = "sk-..."  <- NEVER do this
# Add secrets via CLI — encrypted, never in source control
wrangler secret put OPENAI_API_KEY
wrangler secret put DATABASE_PASSWORD

# List existing secrets (values are hidden)
wrangler secret list

# Local dev: create .dev.vars file (git-ignored)
# .dev.vars
OPENAI_API_KEY=sk-...
DATABASE_PASSWORD=...

Access both vars and secrets identically at runtime: env.OPENAI_API_KEY.

7. Local development with wrangler dev

wrangler dev --local simulates KV, D1, R2, and Queues in-process using local SQLite/miniflare. Use --remote only when you need live Cloudflare infrastructure (e.g. Workers AI).

# Local simulation (fast, no network, free)
wrangler dev --local

# Remote — uses your live Cloudflare account (billed)
wrangler dev --remote

# Apply D1 migrations against local dev DB
wrangler d1 migrations apply my-db --local

# Apply D1 migrations against production
wrangler d1 migrations apply my-db --remote

# Inspect local KV contents
wrangler kv key list --binding=MY_KV --local
Secrets in local dev: create a .dev.vars file in the project root (add it to .gitignore). wrangler dev automatically loads it.

Get an email the next time Cloudflare AI goes down

Outage alerts for Cloudflare AI, straight to your inbox. No account needed, unsubscribe in every email.

Watching more than one service? A free account covers 5 services + a daily digest — and Pro is currently free.

FAQ

What is the Cloudflare Workers CPU time limit?

Free tier: 10ms CPU time per invocation (30ms wall-clock). Paid tier (Workers Paid, $5/month): 30 seconds CPU time. CPU time counts only active JS execution — awaiting async I/O like fetch, KV.get, or D1.prepare does not count against the limit.

Why does KV return null right after a put?

Workers KV is eventually consistent. A write propagates globally in up to 60 seconds. An immediate read from a different edge node may still return the old value (or null). If you need read-after-write consistency, use Durable Objects or D1 instead of KV.

Can I use the AWS S3 SDK with R2?

Technically yes via R2's S3-compatible API, but it is not recommended inside a Worker — the SDK is large, wastes CPU startup time, and requires your R2 credentials to be loaded as secrets. Use the native R2 binding (env.MY_BUCKET.put/get/delete) for Worker-to-R2 access. Reserve the S3 SDK for server-side code outside of the Workers runtime.

How do I pass secrets to wrangler dev locally?

Create a .dev.vars file in your project root with one KEY=VALUE per line. Add .dev.vars to .gitignore. Wrangler automatically loads it when running wrangler dev. In CI and production, deploy secrets with wrangler secret put.

Monitor related services