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

1. The bindings catalog (overview)

BindingWhat it isWhen to use
kv_namespacesKV — globally replicated key-value, eventually consistentCaching, config, simple lookups (millions of small keys)
r2_bucketsR2 — S3-compatible object storage, zero egressFiles, assets, backups, data lakes
d1_databasesD1 — SQLite at edge, primary + read replicasRelational data, transactions, joins
durable_objectsDO — strongly-consistent stateful compute with SQLite storagePer-entity state, real-time coordination, multiplayer, rate limiting
queuesQueues — producer/consumer messagingAsync work, batching, decoupled systems
vectorizeVectorize — vector DBEmbeddings, semantic search, RAG
aiWorkers AI — managed LLM inferenceLLM calls, embeddings, image gen
workflowsWorkflows — durable executionMulti-step async with retries, sleeps
hyperdriveHyperdrive — Postgres/MySQL pooling + cachingExternal relational DBs
servicesService Bindings — RPC between WorkersComposing multiple Workers (zero-cost)
browserBrowser Rendering — Puppeteer-in-WorkersPDF generation, screenshots, scraping
analytics_engine_datasetsAnalytics Engine — write-only time-series for custom metricsCustom analytics, observability
dispatch_namespacesWorkers for Platforms — multi-tenant Worker hostingBuild SaaS where customers deploy code
mtls_certificatesmTLS — client cert for outboundConnecting to mTLS-secured upstreams
assetsStatic Assets — files in a directory served by WorkerFull-stack apps (Workers + static frontend)
imagesCloudflare Images — image processingResize, 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.toml

Usage

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

Usage

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 7

Limits

  • 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 automatically

Migrations

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 production

Migration 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 reads

Limits

  • 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 SPAs

In 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 DESC

Free for a generous tier; Paid for higher volume.

13. Generating TypeScript types

wrangler types
# Generates worker-configuration.d.ts with typed Env interface

Include 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-to for disk persistence)
  • R2 — local filesystem (--persist-to)
  • D1 — local SQLite file
  • Durable Objects — local instance
  • Queues — local
  • AI, Vectorize, Browser Rendering, Hyperdrivealways remote (no local emulation)

For full-fidelity testing against production bindings:

wrangler dev --remote

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

Further reading