Skip to content

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

FixDevs ·

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. 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. 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.

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;

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.

For related serverless issues, see Fix: Hono Not Working and Fix: Nitro 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