A binding is a capability injected into env at runtime. You declare it in wrangler.jsonc; the runtime hands you a live client. All config snippets are jsonc; the TOML form is a mechanical translation (kv_namespaces → [[kv_namespaces]], nested objects → [table]).
Verified 2026-06 against developers.cloudflare.com/workers/wrangler/configuration.
| Need | Binding |
|---|---|
| Global cache / config, read-heavy, writes rare | KV |
| Relational data, SQL, joins | D1 |
| Files / blobs / media, no egress fee | R2 |
| Strong consistency, coordination, realtime state | Durable Objects |
| Background jobs, batching, retries | Queues |
| Existing external Postgres/MySQL | Hyperdrive |
| Model inference (LLM/embeddings/image) | Workers AI |
| Vector search / RAG | Vectorize |
| Worker-to-Worker RPC | Service binding |
| Custom metrics | Analytics Engine |
| Static files | Assets |
{ "kv_namespaces": [{ "binding": "CACHE", "id": "<namespace-id>", "preview_id": "<preview-id>" }] }
await env.CACHE.put("key", "value", { expirationTtl: 3600, metadata: { v: 1 } });
const v = await env.CACHE.get("key"); // string | null
const j = await env.CACHE.get("key", { type: "json" });
const { value, metadata } = await env.CACHE.getWithMetadata("key");
const list = await env.CACHE.list({ prefix: "user:" });
await env.CACHE.delete("key");
list are comparatively expensive. Use TTLs; avoid hot list in the request path.wrangler kv namespace create CACHE, wrangler kv key put --binding=CACHE k v.{ "d1_databases": [{ "binding": "DB", "database_name": "app", "database_id": "<d1-id>" }] }
const { results } = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(id).all();
const row = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(id).first();
await env.DB.prepare("INSERT INTO users (name) VALUES (?)").bind(name).run();
await env.DB.batch([stmt1, stmt2]); // batched, atomic
.bind() parameters — never string-interpolate SQL.wrangler d1 migrations create / apply. Local: wrangler d1 execute DB --local --file=schema.sql.{ "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "uploads" }] }
await env.BUCKET.put("path/file.png", request.body, { httpMetadata: { contentType: "image/png" } });
const obj = await env.BUCKET.get("path/file.png");
if (obj) return new Response(obj.body, { headers: { "etag": obj.httpEtag } });
await env.BUCKET.delete("path/file.png");
const listed = await env.BUCKET.list({ prefix: "path/" });
{
"durable_objects": { "bindings": [{ "name": "ROOM", "class_name": "ChatRoom" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatRoom"] }]
}
export class ChatRoom {
constructor(state, env) { this.state = state; this.env = env; }
async fetch(request) {
let count = (await this.state.storage.get("count")) || 0;
await this.state.storage.put("count", ++count); // serialized — no races
return Response.json({ count });
}
}
// From another Worker:
const id = env.ROOM.idFromName("room-42");
const stub = env.ROOM.get(id);
const res = await stub.fetch(request);
new_sqlite_classes in migrations) for relational state per object.tag each migration; never edit an applied one.{
"queues": {
"producers": [{ "binding": "JOBS", "queue": "jobs" }],
"consumers": [{ "queue": "jobs", "max_batch_size": 10, "max_batch_timeout": 30, "dead_letter_queue": "jobs-dlq" }]
}
}
export default {
async fetch(req, env) { await env.JOBS.send({ task: "resize", id: 7 }); return new Response("queued"); },
async queue(batch, env) {
for (const msg of batch.messages) {
try { await handle(msg.body); msg.ack(); }
catch { msg.retry(); } // retried; exhausted → dead-letter queue
}
},
};
ack()/retry() per message or per batch.{
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<hyperdrive-config-id>" }]
}
import postgres from "postgres";
const sql = postgres(env.HYPERDRIVE.connectionString);
const rows = await sql`SELECT * FROM orders WHERE id = ${id}`;
nodejs_compat. Use a Workers-compatible driver (postgres, pg with compat, mysql2).{ "ai": { "binding": "AI" } }
const out = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", { prompt: "Hello" });
const emb = await env.AI.run("@cf/baai/bge-base-en-v1.5", { text: ["doc one", "doc two"] });
ai object binding (no array). Run text-gen, embeddings, image, speech models by ID.{ "vectorize": [{ "binding": "INDEX", "index_name": "docs" }] }
await env.INDEX.upsert([{ id: "1", values: embedding, metadata: { url } }]);
const matches = await env.INDEX.query(queryEmbedding, { topK: 5, returnMetadata: true });
wrangler vectorize create docs --dimensions=768 --metric=cosine.{ "services": [{ "binding": "AUTH", "service": "auth-worker", "entrypoint": "AuthEntrypoint" }] }
const ok = await env.AUTH.verify(token); // RPC method (WorkerEntrypoint) — no network hop
const res = await env.AUTH.fetch(internalRequest); // or HTTP-style
WorkerEntrypoint) or fetch-style.{ "analytics_engine_datasets": [{ "binding": "AE", "dataset": "my_metrics" }] }
env.AE.writeDataPoint({ blobs: [country], doubles: [latencyMs], indexes: [route] });
{ "assets": { "directory": "./public", "binding": "ASSETS",
"html_handling": "auto-trailing-slash", "not_found_handling": "single-page-application" } }
// In a Worker with both `main` and `assets`:
export default {
async fetch(request, env) {
if (new URL(request.url).pathname.startsWith("/api/")) return handleApi(request, env);
return env.ASSETS.fetch(request); // serve static files
},
};
main) = pure static host; matching requests never invoke Worker code (and aren't billed as invocations).not_found_handling: "single-page-application" serves index.html for unmatched routes (SPA routing); "404-page" serves a custom 404.run_worker_first: true runs your Worker before asset matching (for auth gates, rewrites).