Workers bindings and storage
A binding is Cloudflare’s term for a typed handle to an external resource — a KV namespace, an R2 bucket, a D1 database, a Durable Object class, a Queue, a Vectorize index, the AI runtime — that’s wired into your Worker at deploy time. The binding object on env has methods to access the resource without credentials: Cloudflare handles auth automatically based on which account the Worker is deployed to. This is the central abstraction that makes Workers feel like a unified platform rather than a serverless function calling other services over HTTP.
See also
- durable-objects-and-sqlite-storage — DO bindings get their own deep dive
- workers-ai-and-agents — AI + Vectorize bindings get their own deep dive
- workers-workflows-and-async — Queues + Workflows + Cron Triggers
- sql-nosql-design — choosing between KV (k/v) and D1 (SQL) for your data
1. The bindings catalog (overview)
| Binding | What it is | When to use |
|---|---|---|
kv_namespaces | KV — globally replicated key-value, eventually consistent | Caching, config, simple lookups (millions of small keys) |
r2_buckets | R2 — S3-compatible object storage, zero egress | Files, assets, backups, data lakes |
d1_databases | D1 — SQLite at edge, primary + read replicas | Relational data, transactions, joins |
durable_objects | DO — strongly-consistent stateful compute with SQLite storage | Per-entity state, real-time coordination, multiplayer, rate limiting |
queues | Queues — producer/consumer messaging | Async work, batching, decoupled systems |
vectorize | Vectorize — vector DB | Embeddings, semantic search, RAG |
ai | Workers AI — managed LLM inference | LLM calls, embeddings, image gen |
workflows | Workflows — durable execution | Multi-step async with retries, sleeps |
hyperdrive | Hyperdrive — Postgres/MySQL pooling + caching | External relational DBs |
services | Service Bindings — RPC between Workers | Composing multiple Workers (zero-cost) |
browser | Browser Rendering — Puppeteer-in-Workers | PDF generation, screenshots, scraping |
analytics_engine_datasets | Analytics Engine — write-only time-series for custom metrics | Custom analytics, observability |
dispatch_namespaces | Workers for Platforms — multi-tenant Worker hosting | Build SaaS where customers deploy code |
mtls_certificates | mTLS — client cert for outbound | Connecting to mTLS-secured upstreams |
assets | Static Assets — files in a directory served by Worker | Full-stack apps (Workers + static frontend) |
images | Cloudflare Images — image processing | Resize, crop, format conversion |
2. KV — eventually-consistent global k/v
Per developers.cloudflare.com/kv/. KV is replicated to all 330+ POPs. Reads from the nearest POP are typically < 10 ms; writes propagate globally in ~60 seconds (eventually consistent).
Config
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456"
preview_id = "preview-id"Or via CLI:
wrangler kv namespace create CACHE
# wrangler reports the IDs to add to wrangler.tomlUsage
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Read
const value = await env.CACHE.get("user:123"); // string | null
const obj = await env.CACHE.get("user:123", { type: "json" }); // any | null
const buf = await env.CACHE.get("blob:abc", { type: "arrayBuffer" });
// Write
await env.CACHE.put("user:123", JSON.stringify({ name: "Adam" }), {
expirationTtl: 3600, // expire in 1 hour
metadata: { tier: "pro" }, // small associated metadata
});
// Delete
await env.CACHE.delete("user:123");
// List
const list = await env.CACHE.list({ prefix: "user:", limit: 1000 });
for (const key of list.keys) {
console.log(key.name, key.metadata);
}
// Read with metadata
const { value: v, metadata: m } = await env.CACHE.getWithMetadata("user:123");
return new Response("OK");
},
};Limits
- Max key size: 512 bytes
- Max value size: 25 MB
- Max metadata size: 1024 bytes
- Max keys per namespace: unlimited
- Max namespaces per account: 1000
- Eventual consistency: ~60 sec to propagate writes globally
Pricing (Paid)
- Reads: 10M / month included, then $0.50 / M
- Writes / list / delete: 1M / month included, then $5 / M
- Storage: 1 GB included, then $0.50 / GB-month
3. R2 — S3-compatible, zero egress
Per developers.cloudflare.com/r2/. R2 is object storage compatible with the S3 API. The defining feature: no egress fees. Reading data out of R2 to anywhere (your servers, end users, other clouds) is free.
Config
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-app-assets"wrangler r2 bucket create my-app-assetsUsage
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === "PUT") {
await env.ASSETS.put(key, request.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") {
const obj = await env.ASSETS.get(key);
if (!obj) return new Response("Not found", { status: 404 });
const headers = new Headers();
obj.writeHttpMetadata(headers);
headers.set("etag", obj.httpEtag);
return new Response(obj.body, { headers });
}
if (request.method === "DELETE") {
await env.ASSETS.delete(key);
return new Response(null, { status: 204 });
}
// List
const list = await env.ASSETS.list({ prefix: "images/", limit: 1000 });
return Response.json(list);
},
};S3-compatible API for external tools
# Use aws-sdk or aws-cli with R2 endpoint
aws s3 ls --endpoint-url https://<account>.r2.cloudflarestorage.com s3://my-app-assets/Generate access keys in the dashboard → R2 → Manage R2 API Tokens.
Lifecycle rules + Object Lifecycle
wrangler r2 bucket lifecycle add my-app-assets \
--condition-prefix "temp/" \
--action delete --action-after-days 7Limits
- Max object size: 5 TB
- Max objects per bucket: unlimited
- Max buckets per account: 1000
Pricing (Paid)
- Storage: 10 GB / month free, then $0.015 / GB-month
- Class A operations (writes, lists): 1M / month free, then $4.50 / M
- Class B operations (reads): 10M / month free, then $0.36 / M
- Egress: $0 (the headline feature)
For typical static-asset workloads, R2 is 60-90% cheaper than S3 + CloudFront.
4. D1 — SQLite at edge
Per developers.cloudflare.com/d1/. SQLite databases hosted on Cloudflare’s infrastructure with primary region + read replicas in other regions. GA in early 2024; Free tier moved to GA pricing in late 2024.
Config
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "uuid-here"wrangler d1 create my-db
# Adds the ID to wrangler.toml automaticallyMigrations
wrangler d1 migrations create my-db init
# Edits migrations/0001_init.sql
wrangler d1 migrations apply my-db --local # apply to local dev DB
wrangler d1 migrations apply my-db --remote # apply to productionMigration SQL:
-- migrations/0001_init.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_users_email ON users(email);Querying
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Prepared statements (parameterized)
const stmt = env.DB.prepare("SELECT * FROM users WHERE email = ?");
const user = await stmt.bind("adam@example.com").first();
// Multiple rows
const allUsers = await env.DB.prepare("SELECT * FROM users LIMIT 100").all();
console.log(allUsers.results); // array of rows
console.log(allUsers.meta); // { duration, served_by, ... }
// Insert
const result = await env.DB.prepare(
"INSERT INTO users (email, name) VALUES (?, ?)"
).bind("new@example.com", "New User").run();
console.log(result.meta.last_row_id);
// Transactions (D1 batches)
const batchResults = await env.DB.batch([
env.DB.prepare("INSERT INTO users (email, name) VALUES (?, ?)").bind("a@x.com", "A"),
env.DB.prepare("INSERT INTO users (email, name) VALUES (?, ?)").bind("b@x.com", "B"),
]);
return Response.json({ user, allUsers, batchResults });
},
};Read replicas (Sessions API)
Per re:Invent 2024. D1 supports read replicas in multiple regions. Use the Sessions API to get strong-read-your-writes consistency across reads:
const session = env.DB.withSession("first-unconstrained");
// First query — may go to any replica
const a = await session.prepare("SELECT * FROM users").all();
// After a write, subsequent reads on this session see the write
await session.prepare("INSERT INTO logs VALUES (?)").bind("event").run();
const b = await session.prepare("SELECT * FROM logs ORDER BY id DESC LIMIT 1").all();
// b sees the inserted row even if a different replica usually serves readsLimits
- Max DB size: 10 GB (Paid)
- Max query duration: 30 seconds
- Max rows per response: 10000 (configurable up to 1M)
- Max prepared statement bind size: 25 MB
- Max DBs per account: 50000 (Paid)
Pricing (Paid)
- Rows read: 25 B / month included, then $0.001 / M
- Rows written: 50 M / month included, then $1 / M
- Storage: 5 GB / month included, then $0.75 / GB-month
D1 is dramatically cheaper than RDS / Aurora for typical web workloads.
5. Durable Objects (covered in depth elsewhere)
See durable-objects-and-sqlite-storage.
[[durable_objects.bindings]]
name = "ROOMS"
class_name = "Room"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["Room"] # SQLite-backed DO (default 2024+)const id = env.ROOMS.idFromName("room-42");
const room = env.ROOMS.get(id);
const resp = await room.fetch("https://room/state");6. Queues
Per developers.cloudflare.com/queues/.
Config
# Producer side (sending Worker)
[[queues.producers]]
binding = "MY_QUEUE"
queue = "my-queue"
# Consumer side (receiving Worker)
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 100
max_batch_timeout = 30
dead_letter_queue = "my-dlq"Producer
export default {
async fetch(request: Request, env: Env): Promise<Response> {
await env.MY_QUEUE.send({ event: "user_signup", userId: "u_123" });
// Or batch
await env.MY_QUEUE.sendBatch([
{ body: { event: "a" } },
{ body: { event: "b" }, delaySeconds: 60 },
]);
return new Response("Queued");
},
};Consumer
export default {
async queue(batch: MessageBatch<{ event: string }>, env: Env): Promise<void> {
for (const message of batch.messages) {
try {
await processEvent(message.body);
message.ack();
} catch (err) {
message.retry({ delaySeconds: 30 });
}
}
},
};Detailed in workers-workflows-and-async.
7. Vectorize
Per developers.cloudflare.com/vectorize/. Managed vector database.
[[vectorize]]
binding = "VECTORS"
index_name = "embeddings"wrangler vectorize create embeddings --dimensions=1024 --metric=cosine// Insert
await env.VECTORS.insert([
{
id: "doc-1",
values: [0.1, 0.2, /* ... 1024 floats */],
metadata: { source: "blog", url: "https://..." },
},
]);
// Query
const results = await env.VECTORS.query(queryEmbedding, {
topK: 5,
returnMetadata: true,
filter: { source: { "$eq": "blog" } },
});Detailed in workers-ai-and-agents.
8. Service Bindings — Worker-to-Worker
Per developers.cloudflare.com/workers/configuration/bindings/about-service-bindings/. Call another Worker without HTTP overhead.
[[services]]
binding = "AUTH"
service = "auth-worker"Two modes:
8.1 Function-style (RPC)
// auth-worker
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async verify(token: string): Promise<{ userId: string } | null> {
return { userId: "u_123" };
}
}
// Caller
const result = await env.AUTH.verify("abc123");8.2 fetch-style
// auth-worker — regular fetch handler
// Caller
const resp = await env.AUTH.fetch("https://auth.internal/verify", {
method: "POST",
body: JSON.stringify({ token: "abc" }),
});Service Bindings are free — they don’t count as subrequests, don’t bill as separate requests. Composing 10 Workers via service bindings costs the same as one Worker with 10 functions.
9. Browser Rendering
Per developers.cloudflare.com/browser-rendering/. Puppeteer running on Cloudflare’s GPUs. Used for PDF generation, screenshots, scraping, end-to-end testing.
[browser]
binding = "BROWSER"import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await puppeteer.launch(env.BROWSER);
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot({ type: "png" });
await browser.close();
return new Response(screenshot, { headers: { "Content-Type": "image/png" } });
},
};Pricing: per-minute-of-browser-time. Free tier limited; Paid usage-based.
10. Static Assets binding (Workers as full-stack)
Per developers.cloudflare.com/workers/static-assets/.
[assets]
directory = "./dist"
binding = "ASSETS"
not_found_handling = "single-page-application" # for SPAsIn the Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// API routes — handle in Worker
if (url.pathname.startsWith("/api/")) {
return new Response(JSON.stringify({ ok: true }));
}
// Everything else — serve from static assets
return env.ASSETS.fetch(request);
},
};This is Cloudflare’s converged path: Workers as the host for both APIs and the static frontend. Replaces older “Pages Functions” pattern.
11. mTLS certificates
For outbound calls that need a client certificate:
[[mtls_certificates]]
binding = "CLIENT_CERT"
certificate_id = "abc123"const response = await fetch("https://secure-upstream.example.com/api", {
// Cloudflare uses the bound cert automatically when the hostname matches
});12. Analytics Engine — custom metrics
Per developers.cloudflare.com/analytics/analytics-engine/. Write-only time-series store for custom metrics:
[[analytics_engine_datasets]]
binding = "ANALYTICS"
dataset = "app_metrics"env.ANALYTICS.writeDataPoint({
blobs: [request.cf?.country ?? "unknown", url.pathname], // dimensions
doubles: [response.status, latencyMs], // measurements
indexes: ["api"], // 1-cardinality index
});Query via SQL API:
SELECT blob1 as country, AVG(double2) as p_latency
FROM app_metrics
WHERE timestamp > now() - INTERVAL 1 HOUR
GROUP BY blob1
ORDER BY p_latency DESCFree for a generous tier; Paid for higher volume.
13. Generating TypeScript types
wrangler types
# Generates worker-configuration.d.ts with typed Env interfaceInclude in your tsconfig:
{
"compilerOptions": {
"types": ["@cloudflare/workers-types/2024-05-01"]
}
}14. Local dev with bindings
wrangler dev (local mode by default) emulates most bindings locally:
- KV — in-memory by default (
--persist-tofor disk persistence) - R2 — local filesystem (
--persist-to) - D1 — local SQLite file
- Durable Objects — local instance
- Queues — local
- AI, Vectorize, Browser Rendering, Hyperdrive — always remote (no local emulation)
For full-fidelity testing against production bindings:
wrangler dev --remote15. Multiple environments
name = "my-worker"
main = "src/index.ts"
[[kv_namespaces]]
binding = "CACHE"
id = "prod-id"
preview_id = "preview-id"
[env.staging]
name = "my-worker-staging"
[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-id"Deploy with wrangler deploy --env staging.