Fix: Wrangler Not Working — Deploy Failing, Bindings Not Found, or D1 Queries Returning Empty
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Wrangler and Cloudflare Workers issues — wrangler.toml configuration, KV and D1 bindings, R2 storage, environment variables, local dev with Miniflare, and deployment troubleshooting.
The Problem
wrangler dev starts but bindings are undefined:
export default {
async fetch(request: Request, env: Env) {
const value = await env.MY_KV.get('key');
// TypeError: Cannot read properties of undefined (reading 'get')
},
};Or D1 queries return empty even after inserting data:
const result = await env.DB.prepare('SELECT * FROM users').all();
// { results: [] } — but data was inserted moments agoOr deployment fails with a cryptic error:
npx wrangler deploy
# Error: Could not resolve "node:buffer"Or environment variables are undefined in production:
const apiKey = env.API_KEY;
// undefined — even though it was set with wrangler secret putWhy This Happens
Wrangler is the CLI for Cloudflare Workers. It manages local development, builds, and deployment. The same wrangler.toml (or wrangler.jsonc in v3.91+) is used to drive Workers, Pages Functions, Containers, and Workers for Platforms — but each product has its own conventions for bindings, build output, and runtime flags. Most “Wrangler not working” reports stem from configuration that targets one product running under another, or from version drift between Wrangler 3 and Wrangler 4.
- Bindings must be declared in
wrangler.toml— KV namespaces, D1 databases, R2 buckets, and other resources must be explicitly bound in the config file. Theenvparameter in your fetch handler only contains bindings listed inwrangler.toml. A typo or missing declaration means the binding isundefined. - D1 local and remote are separate databases —
wrangler devuses a local SQLite file for D1.wrangler deployconnects to the remote D1 instance. Data inserted during local dev doesn’t exist in production. Migrations must be applied to both. - Workers don’t have Node.js APIs —
node:buffer,node:fs,node:path, and most Node.js built-ins don’t exist in the Workers runtime. Dependencies that use these APIs fail at deploy time. Thenodejs_compatcompatibility flag enables a subset of Node.js APIs. - Secrets are separate from
wrangler.tomlvars —[vars]inwrangler.tomlare plaintext and committed to git. Secrets set viawrangler secret putare encrypted and only available in the deployed Worker. During local dev, secrets must be in.dev.vars.
A second source of confusion is the Pages-vs-Workers split. Cloudflare Pages projects historically had no wrangler.toml — bindings were configured in the Cloudflare dashboard. Since late 2024 Pages supports wrangler.toml natively, but the schema is a subset of Workers’ schema (no main, no triggers.crons, and the [pages_build_output_dir] key replaces webDir). A config copied from a Workers project will fail under Pages with errors like “Unsupported field” or silently ignore bindings.
A third source is Wrangler 4. Wrangler 4 (March 2025) tightened compatibility flag handling, made nodejs_compat_v2 the recommended flag for new projects, and deprecated several inline-secret flows. Projects that ran fine on Wrangler 3.x can suddenly fail to deploy after a npm update if the compatibility date is older than 2024-09-23 (the date that toggled the legacy Node compat behavior).
Fix 1: Configure wrangler.toml Correctly
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"] # Enable Node.js API subset
# Plain-text environment variables (committed to git)
[vars]
ENVIRONMENT = "production"
APP_NAME = "My App"
# KV Namespace binding
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123def456"
preview_id = "xyz789" # Optional: separate KV for wrangler dev
# D1 Database binding
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# R2 Bucket binding
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "my-app-files"
# Durable Objects
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
# Queue producer
[[queues.producers]]
queue = "my-queue"
binding = "MY_QUEUE"
# Queue consumer
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 10
max_batch_timeout = 30
# Cron triggers
[triggers]
crons = ["0 */6 * * *"] # Every 6 hours
# Environment-specific overrides
[env.staging]
name = "my-worker-staging"
[env.staging.vars]
ENVIRONMENT = "staging"
[env.staging.d1_databases]
binding = "DB"
database_name = "my-app-db-staging"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"// src/index.ts — type your bindings
interface Env {
// KV
MY_KV: KVNamespace;
// D1
DB: D1Database;
// R2
STORAGE: R2Bucket;
// Durable Objects
COUNTER: DurableObjectNamespace;
// Queues
MY_QUEUE: Queue<any>;
// Vars
ENVIRONMENT: string;
APP_NAME: string;
// Secrets (set via wrangler secret put)
API_KEY: string;
DATABASE_URL: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// All bindings are typed and available
return new Response('OK');
},
};Fix 2: KV Storage Operations
# Create a KV namespace
npx wrangler kv namespace create MY_KV
# Output: Add to wrangler.toml: [[kv_namespaces]] binding = "MY_KV" id = "..."
# Create preview namespace for local dev
npx wrangler kv namespace create MY_KV --preview// KV operations in a Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Write
await env.MY_KV.put('user:123', JSON.stringify({ name: 'Alice', role: 'admin' }), {
expirationTtl: 3600, // Expire in 1 hour
metadata: { version: 1 },
});
// Read
const user = await env.MY_KV.get('user:123', 'json');
// user = { name: 'Alice', role: 'admin' }
// Read with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('user:123', 'json');
// List keys
const list = await env.MY_KV.list({ prefix: 'user:', limit: 100 });
// list.keys = [{ name: 'user:123', ... }, ...]
// Delete
await env.MY_KV.delete('user:123');
return Response.json({ user });
},
};Fix 3: D1 Database — Migrations and Queries
# Create a D1 database
npx wrangler d1 create my-app-db
# Output: database_id to add to wrangler.toml
# Create a migration
npx wrangler d1 migrations create my-app-db create-users-table-- migrations/0001_create-users-table.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT DEFAULT 'user',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_users_email ON users(email);# Apply migrations locally (for wrangler dev)
npx wrangler d1 migrations apply my-app-db --local
# Apply migrations to remote (production)
npx wrangler d1 migrations apply my-app-db --remote
# Interactive SQL console
npx wrangler d1 execute my-app-db --local --command "SELECT * FROM users"// D1 queries in a Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Prepared statement with parameters (prevents SQL injection)
const user = await env.DB
.prepare('SELECT * FROM users WHERE email = ?')
.bind('[email protected]')
.first(); // Returns first row or null
// Insert
const result = await env.DB
.prepare('INSERT INTO users (email, name) VALUES (?, ?)')
.bind('[email protected]', 'Bob')
.run();
// result.meta.last_row_id, result.meta.changes
// Select all
const { results: users } = await env.DB
.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ?')
.bind(50)
.all();
// Batch — multiple statements in one round trip
const batchResults = await env.DB.batch([
env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind('[email protected]', 'Charlie'),
env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind('[email protected]', 'Diana'),
env.DB.prepare('SELECT COUNT(*) as count FROM users'),
]);
return Response.json({ users });
},
};Fix 4: R2 Object Storage
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1); // Remove leading /
if (request.method === 'PUT') {
// Upload file
const body = await request.arrayBuffer();
await env.STORAGE.put(key, body, {
httpMetadata: {
contentType: request.headers.get('content-type') || 'application/octet-stream',
},
customMetadata: {
uploadedBy: 'user-123',
},
});
return new Response('Uploaded', { status: 201 });
}
if (request.method === 'GET') {
// Download file
const object = await env.STORAGE.get(key);
if (!object) return new Response('Not Found', { status: 404 });
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=86400',
'ETag': object.httpEtag,
},
});
}
if (request.method === 'DELETE') {
await env.STORAGE.delete(key);
return new Response('Deleted');
}
// List objects
const listed = await env.STORAGE.list({ prefix: 'uploads/', limit: 100 });
const files = listed.objects.map(obj => ({
key: obj.key,
size: obj.size,
uploaded: obj.uploaded,
}));
return Response.json({ files });
},
};Fix 5: Local Development and Secrets
# .dev.vars — local secrets (add to .gitignore)
API_KEY=sk-test-12345
DATABASE_URL=postgres://localhost/mydb
WEBHOOK_SECRET=whsec_abc123# Set production secrets
npx wrangler secret put API_KEY
# Prompts for the value — stored encrypted
# List secrets
npx wrangler secret list
# Delete a secret
npx wrangler secret delete API_KEY# Local development
npx wrangler dev # Start local dev server
npx wrangler dev --remote # Use remote bindings (real KV, D1, R2)
npx wrangler dev --local # Force local (default)
npx wrangler dev --port 8787 # Custom port
# Tail logs from production
npx wrangler tail # Stream real-time logs
npx wrangler tail --format pretty # Formatted outputFix 6: Deploy and Environment Management
# Deploy to production
npx wrangler deploy
# Deploy to staging environment
npx wrangler deploy --env staging
# Rollback to previous deployment
npx wrangler rollback
# Check deployment status
npx wrangler deployments listHono framework on Workers:
// src/index.ts — Hono is the most popular Workers framework
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt';
type Bindings = {
DB: D1Database;
MY_KV: KVNamespace;
API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.use('/api/*', cors());
app.get('/api/users', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT * FROM users')
.all();
return c.json({ users: results });
});
app.post('/api/users', async (c) => {
const { email, name } = await c.req.json();
await c.env.DB
.prepare('INSERT INTO users (email, name) VALUES (?, ?)')
.bind(email, name)
.run();
return c.json({ created: true }, 201);
});
export default app;Fix 7: Platform Differences — Workers, Pages, Containers, Wrangler 3 vs 4
Each Cloudflare runtime has its own bindings model. Mixing them produces the most common “binding not found” reports.
Cloudflare Workers. The classic target. main points at a single entrypoint, all bindings are declared in wrangler.toml, and the entrypoint exports a default object with fetch, scheduled, queue, or email handlers. wrangler deploy uploads the bundle to the global edge. This is the only product that supports cron triggers and Durable Objects out of the box.
Cloudflare Pages with Functions. Pages is for static sites that may include serverless functions under /functions. The build output directory is set with pages_build_output_dir and main is not used. Pages Functions historically configured bindings via the dashboard; the wrangler.toml flow is newer and only supports a subset (no triggers.crons, no [[migrations]] block for Durable Objects). To run cron-like work from Pages you have to deploy a sibling Worker.
Cloudflare Containers. Containers (in open beta) wrap an OCI image and run it on demand. Wrangler configures them via [[containers]] with an image and an instances count. Bindings are exposed via environment variables inside the container rather than as a typed env object, which is the opposite of Workers. If you copy a Workers config to a Containers project, all the env.X accesses become undefined because the runtime contract is different.
Bindings per environment. D1, R2, and Durable Objects all need separate IDs per environment, but [vars] and KV bindings can be shared. Define dev/staging/prod under [env.<name>] blocks. A frequent oversight: declaring a binding only at the top level and then wrangler deploy --env staging — staging gets no bindings at all because top-level bindings do not inherit into named environments.
Wrangler 3 vs Wrangler 4. Wrangler 4 (March 2025) changed several defaults:
nodejs_compat_v2replacesnodejs_compatfor new projects and is auto-enabled whencompatibility_date >= 2024-09-23.wrangler dev --remoteno longer requires--experimental-public-data; remote bindings are real by default.wrangler.jsoncis the preferred config format alongsidewrangler.toml.- Inline secret passing via
--var SECRET=...is removed; use.dev.varsorwrangler secret put.
If a project worked on Wrangler 3 and breaks after upgrading, the first checks are: bump compatibility_date to a 2025 value, swap nodejs_compat for nodejs_compat_v2, and replace any --var flags with .dev.vars.
Node compat is not full Node. Even with nodejs_compat_v2, packages that touch child_process, native modules, fs.watch, or anything ABI-specific (e.g., sharp, canvas) cannot run in Workers. The fix is to find a Web-API alternative (@cf-wasm/photon for images) or move that part of the workload to a regular VM.
Still Not Working?
env.BINDING is undefined — the binding name in wrangler.toml must match exactly. binding = "MY_KV" means env.MY_KV, not env.my_kv or env.MyKv. Check for typos and case sensitivity. Also verify you ran npx wrangler dev after editing wrangler.toml — the dev server doesn’t hot-reload config changes.
D1 data exists locally but not in production — local dev uses a SQLite file at .wrangler/state/v3/d1/. Remote D1 is a separate database. Apply migrations to both: --local for dev, --remote for production. Data inserted during wrangler dev stays local.
“Could not resolve node:X” on deploy — your code or a dependency uses Node.js APIs. Add compatibility_flags = ["nodejs_compat"] to wrangler.toml. This enables Buffer, crypto, util, events, and other common APIs. If the specific API isn’t supported, find a Web API alternative or use a polyfill.
Secrets work in production but are empty in wrangler dev — wrangler secret put only sets production secrets. For local development, create a .dev.vars file in your project root with KEY=value pairs. Add .dev.vars to .gitignore.
wrangler deploy --env staging deploys but bindings are missing — top-level bindings do not inherit into named environments. Re-declare [[d1_databases]], [[kv_namespaces]], and [vars] under [env.staging] with the staging IDs.
Pages Functions don’t see the bindings configured in the dashboard — Pages mixes two sources: the dashboard and wrangler.toml. If wrangler.toml exists in the project root, Pages uses it exclusively and ignores the dashboard for that environment. Either move all bindings into wrangler.toml or delete it from the build output.
Worker hits CPU time limits after upgrading to Wrangler 4 — the bundled output changed, often pulling in larger polyfills under nodejs_compat_v2. Profile with wrangler dev --inspect and consider switching the heavy code path to native Web APIs. Long-running work belongs in a Queue consumer or Durable Object instead of the request handler.
For related serverless issues, see Fix: Hono Not Working, Fix: Nitro Not Working, Fix: Cloudflare D1 Not Working, and Fix: Cloudflare Pages Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Hono Not Working — Route Not Matching, Middleware Skipped, or RPC Client Type Mismatch
How to fix Hono framework issues — routing order, middleware chaining, Hono RPC type inference, Cloudflare Workers bindings, validator integration, and runtime compatibility.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.