Miniflare vs Wrangler vs Workers SDK: Local Cloudflare Workers Dev (2026)
TL;DR
Wrangler is the official Cloudflare CLI for developing, testing, and deploying Workers — it's the entry point for everything Cloudflare Workers-related. Miniflare is the local Workers simulator that Wrangler uses internally — you can use it directly in tests via its API. @cloudflare/workers-types + @cloudflare/workers-sdk provide the TypeScript types and programmatic access to Workers features. In 2026: use wrangler dev for local development, use Miniflare directly for unit testing Workers, and use @cloudflare/workers-types for TypeScript type safety.
Key Takeaways
- wrangler: ~1M weekly downloads — official Cloudflare CLI,
wrangler devruns Workers locally - miniflare: ~300K weekly downloads — local Workers simulator, used by Wrangler internally
- @cloudflare/workers-types: ~3M weekly downloads — TypeScript types for the Workers runtime
- Wrangler 3+ embeds Miniflare —
wrangler devis powered by Miniflare under the hood - Miniflare simulates KV, D1, R2, Durable Objects, Queues locally — no actual Cloudflare account needed
- Workers run on V8 — not Node.js, so Node APIs (fs, path, etc.) are NOT available
The Cloudflare Workers Stack
Cloudflare Workers runtime:
- V8 JavaScript engine (NOT Node.js)
- Limited runtime: no fs, no child_process, no most Node built-ins
- 128MB memory limit, 10ms CPU per request (Bundled plan)
- Runs at the edge: 300+ locations worldwide
APIs available in Workers:
KV → key-value store (eventually consistent, global)
D1 → SQLite database (strong consistency, regional)
R2 → S3-compatible object storage (no egress fees)
Durable Objects → stateful objects with strict consistency
Queues → message queues with delivery guarantees
Hyperdrive → connection pooling for Postgres/MySQL
AI → inference API for AI models
Browser Rendering → headless browser API
Local development tools:
wrangler dev → serves worker locally with Miniflare
miniflare → programmatic Workers testing
Wrangler
Wrangler — the official Cloudflare Workers CLI:
Project setup
# Create a new Workers project:
npm create cloudflare@latest my-worker
# Or install wrangler and initialize:
npm install -D wrangler
npx wrangler init my-worker
# Project structure:
# src/index.ts — Worker code
# wrangler.toml — Configuration
# package.json
wrangler.toml
name = "pkgpulse-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
node_compat = false # Disable Node.js compatibility layer
# KV namespace binding:
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456" # Production KV namespace ID
preview_id = "xyz789" # Local dev KV namespace (or auto-created)
# D1 database binding:
[[d1_databases]]
binding = "DB"
database_name = "pkgpulse"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# R2 bucket binding:
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "pkgpulse-assets"
# Environment variables (plain values — use secrets for sensitive):
[vars]
ENVIRONMENT = "production"
API_URL = "https://api.pkgpulse.com"
# Secrets (stored encrypted, set via CLI):
# wrangler secret put API_KEY
The Worker itself
// src/index.ts — exports a default fetch handler:
export interface Env {
// Bindings from wrangler.toml:
CACHE: KVNamespace
DB: D1Database
ASSETS: R2Bucket
API_KEY: string
ENVIRONMENT: string
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// Route handler:
if (url.pathname === "/api/packages") {
return handlePackages(request, env, ctx)
}
if (url.pathname === "/api/health") {
return Response.json({ status: "ok", env: env.ENVIRONMENT })
}
return new Response("Not Found", { status: 404 })
},
}
async function handlePackages(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url)
const name = url.searchParams.get("name")
if (!name) {
return Response.json({ error: "name is required" }, { status: 400 })
}
// Check KV cache:
const cached = await env.CACHE.get(`package:${name}`, { type: "json" })
if (cached) {
return Response.json(cached, {
headers: { "X-Cache": "HIT" },
})
}
// Query D1:
const result = await env.DB.prepare(
"SELECT * FROM packages WHERE name = ?"
).bind(name).first()
if (!result) {
return Response.json({ error: "Package not found" }, { status: 404 })
}
// Cache in KV (background, doesn't block response):
ctx.waitUntil(
env.CACHE.put(`package:${name}`, JSON.stringify(result), {
expirationTtl: 5 * 60, // 5 minutes
})
)
return Response.json(result)
}
Local development
# Start local dev server (powered by Miniflare):
npx wrangler dev
# Local dev features:
# - Hot reload on file changes
# - Simulates KV, D1, R2, Durable Objects locally
# - Available at http://localhost:8787
# - Remote mode (--remote) uses actual Cloudflare edge
# Run with live D1 data (remote):
npx wrangler dev --remote
# Deploy to production:
npx wrangler deploy
Miniflare
Miniflare — local Cloudflare Workers simulator:
Use Miniflare for testing
// tests/worker.test.ts — using Miniflare directly:
import { Miniflare } from "miniflare"
import { describe, test, expect, beforeAll, afterAll } from "vitest"
let mf: Miniflare
beforeAll(async () => {
mf = new Miniflare({
script: `
export default {
async fetch(request, env) {
const name = new URL(request.url).searchParams.get("name")
const pkg = await env.CACHE.get(name)
if (pkg) return Response.json(JSON.parse(pkg))
return new Response("Not Found", { status: 404 })
}
}
`,
modules: true, // Use ES modules
kvNamespaces: ["CACHE"], // Simulate KV
// d1Databases: ["DB"], // Simulate D1
// r2Buckets: ["ASSETS"], // Simulate R2
})
})
afterAll(async () => {
await mf.dispose()
})
test("returns cached package", async () => {
// Seed KV:
const kv = await mf.getKVNamespace("CACHE")
await kv.put("react", JSON.stringify({ name: "react", score: 92.5 }))
// Make a request:
const response = await mf.dispatchFetch("http://localhost/?name=react")
expect(response.status).toBe(200)
const data = await response.json()
expect(data.name).toBe("react")
expect(data.score).toBe(92.5)
})
test("returns 404 for unknown package", async () => {
const response = await mf.dispatchFetch("http://localhost/?name=nonexistent")
expect(response.status).toBe(404)
})
Loading from wrangler.toml
import { Miniflare } from "miniflare"
// Load Miniflare config directly from wrangler.toml:
const mf = new Miniflare({
scriptPath: "src/index.ts",
wranglerConfigPath: "wrangler.toml", // Use real wrangler config
modules: true,
// Override for tests:
kvNamespaces: ["CACHE"],
d1Databases: ["DB"],
})
D1 testing
import { Miniflare } from "miniflare"
const mf = new Miniflare({
modules: true,
d1Databases: ["DB"],
script: `
export default {
async fetch(request, env) {
const { results } = await env.DB.prepare(
"SELECT * FROM packages WHERE health_score > 80"
).all()
return Response.json(results)
}
}
`,
})
// Setup D1 schema and seed data:
const db = await mf.getD1Database("DB")
await db.exec(`
CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT, health_score REAL);
INSERT INTO packages VALUES (1, 'react', 92.5);
INSERT INTO packages VALUES (2, 'vue', 89.0);
INSERT INTO packages VALUES (3, 'legacy', 45.0);
`)
const response = await mf.dispatchFetch("http://localhost/")
const data = await response.json()
// [{ id: 1, name: "react", health_score: 92.5 }, { id: 2, name: "vue", health_score: 89 }]
@cloudflare/workers-types
// Install TypeScript types for the Workers runtime:
// npm install -D @cloudflare/workers-types
// tsconfig.json:
{
"compilerOptions": {
"lib": ["es2022"],
"types": ["@cloudflare/workers-types/2023-07-01"] // Pin to a compat date
}
}
// Now you get proper types for KV, D1, R2, fetch, Request, Response, etc.:
const kv: KVNamespace = env.CACHE
const db: D1Database = env.DB
const bucket: R2Bucket = env.ASSETS
// KV types:
await kv.put("key", "value", { expirationTtl: 3600 })
const value: string | null = await kv.get("key")
// D1 types:
const stmt: D1PreparedStatement = db.prepare("SELECT * FROM packages")
const result: D1Result<{ name: string; score: number }> = await stmt.all()
Feature Comparison
| Feature | wrangler | miniflare | @cf/workers-types |
|---|---|---|---|
| Local dev server | ✅ | ✅ (programmatic) | ❌ |
| Deploy to Cloudflare | ✅ | ❌ | ❌ |
| Unit testing API | ❌ | ✅ | ❌ |
| KV simulation | ✅ | ✅ | types only |
| D1 simulation | ✅ | ✅ | types only |
| R2 simulation | ✅ | ✅ | types only |
| TypeScript types | ❌ | ❌ | ✅ |
| Hot reload | ✅ | ❌ | ❌ |
| Secrets management | ✅ | ❌ | ❌ |
| Weekly downloads | ~1M | ~300K | ~3M |
When to Use Each
Use wrangler for:
- Local development (
wrangler dev) - Deploying to Cloudflare (
wrangler deploy) - Managing secrets, KV namespaces, D1 databases (
wrangler kv,wrangler d1) - Generating type definitions (
wrangler types)
Use Miniflare for:
- Unit testing Workers in Vitest/Jest
- Integration tests that need simulated KV/D1/R2
- CI environments (no actual Cloudflare account needed)
Use @cloudflare/workers-types for:
- TypeScript type safety in your Worker code
- Autocomplete for
KVNamespace,D1Database,R2Bucket,ExecutionContext
All three together (recommended setup):
{
"devDependencies": {
"wrangler": "^3.0.0",
"miniflare": "^3.0.0",
"@cloudflare/workers-types": "^4.0.0",
"vitest": "^2.0.0"
}
}
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on wrangler v3.x, miniflare v3.x, and @cloudflare/workers-types v4.x.
Compare edge computing and serverless packages on PkgPulse →