Fix: Wrangler Not Working — Deploy Failing, Bindings Not Found, or D1 Queries Returning Empty
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. Common issues stem from:
- 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.
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;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.
For related serverless issues, see Fix: Hono Not Working and Fix: Nitro 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.