Skip to content

Fix: Wrangler Not Working — Deploy Failing, Bindings Not Found, or D1 Queries Returning Empty

FixDevs · (Updated: )

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 ago

Or 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 put

Why 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. The env parameter in your fetch handler only contains bindings listed in wrangler.toml. A typo or missing declaration means the binding is undefined.
  • D1 local and remote are separate databaseswrangler dev uses a local SQLite file for D1. wrangler deploy connects 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 APIsnode: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. The nodejs_compat compatibility flag enables a subset of Node.js APIs.
  • Secrets are separate from wrangler.toml vars[vars] in wrangler.toml are plaintext and committed to git. Secrets set via wrangler secret put are 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 output

Fix 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 list

Hono 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_v2 replaces nodejs_compat for new projects and is auto-enabled when compatibility_date >= 2024-09-23.
  • wrangler dev --remote no longer requires --experimental-public-data; remote bindings are real by default.
  • wrangler.jsonc is the preferred config format alongside wrangler.toml.
  • Inline secret passing via --var SECRET=... is removed; use .dev.vars or wrangler 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 devwrangler 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles