Cloudflare Workers as a Full-Stack Platform#
Cloudflare started as a CDN and DDoS protection service. It is now a complete development platform. Workers provide serverless compute at 330+ edge locations. D1 provides a serverless SQLite database. KV provides a globally distributed key-value store. R2 provides S3-compatible object storage with zero egress fees. Pages provides static site hosting with git-integrated deploys. Durable Objects provide stateful, single-threaded coordination primitives. Queues provide async message processing between Workers.
Together, these services let you build and deploy a full-stack application – API, database, cache, storage, and static site – without managing any infrastructure. The free tier is generous enough to run production workloads at small scale.
Product Inventory#
| Product | What It Does | Free Tier | Paid Pricing |
|---|---|---|---|
| Workers | Serverless functions, V8 isolates, 330+ PoPs | 100K requests/day | $5/mo base, 10M req included, $0.30/M after |
| D1 | Serverless SQLite database | 5B reads/mo, 100M writes/mo, 5 GB storage | $0.001/M reads, $1/M writes, $0.75/GB/mo |
| KV | Edge key-value store, high read throughput | 100K reads/day, 1K writes/day | $0.50/M reads, $5/M writes |
| R2 | S3-compatible object storage, zero egress | 10 GB storage, 10M Class A/B ops | $0.015/GB/mo, $0 egress |
| Pages | Static site hosting, git deploys | Unlimited bandwidth, 500 builds/mo | Included with Workers paid plan |
| Durable Objects | Stateful serverless, per-entity state | Included in Workers free tier (limited) | $0.15/M requests, $0.50/GB-hr storage |
| Queues | Message queues between Workers | 1M messages/mo | $0.40/M messages |
| Workers AI | ML inference at the edge | 10K neurons/day | Model-specific pricing |
Workers: The Compute Layer#
Workers run JavaScript and TypeScript on V8 isolates – the same engine that powers Chrome. Unlike containers or Lambda, there is no cold start in the traditional sense. V8 isolates spin up in under 5 milliseconds because there is no OS to boot, no runtime to initialize. Your code runs at the edge location nearest to the user.
Execution Model#
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// request: standard Fetch API Request object
// env: bindings to D1, KV, R2, Durable Objects, secrets
// ctx: waitUntil() for async tasks after response
const url = new URL(request.url);
if (url.pathname === "/api/data") {
const results = await env.DB.prepare("SELECT * FROM items LIMIT 10").all();
return Response.json(results);
}
return new Response("Not found", { status: 404 });
},
};Key characteristics:
- Runtime: V8 isolates, Web Standards APIs (Fetch, crypto, streams, URL, Headers)
- Languages: JavaScript, TypeScript, Rust (via WASM), Python (beta)
- Cold start: Sub-5ms (isolate spinup, not container boot)
- CPU limits: 10ms CPU time (free), 30s CPU time (paid) per request
- Memory: 128 MB per isolate
- Request size: 100 MB max
- Global distribution: Runs at 330+ PoPs automatically – no region selection needed
The Env Interface#
Every Cloudflare service is accessed through bindings declared in wrangler.jsonc and available on the env parameter:
export interface Env {
DB: D1Database; // D1 database
CACHE: KVNamespace; // KV namespace
ARTIFACTS: R2Bucket; // R2 bucket
SESSIONS: DurableObjectNamespace; // Durable Objects
QUEUE: Queue; // Queue producer
API_KEY: string; // Secret (env var)
}waitUntil for Async Work#
Use ctx.waitUntil() to run work after the response is sent. The Worker stays alive until the promise resolves, but the user gets their response immediately.
// Log the request without blocking the response
ctx.waitUntil(logRequest(env.DB, request, response.status, Date.now() - start));
return response;D1: Serverless SQLite#
D1 is a serverless relational database built on SQLite. You define schemas with standard SQL, run migrations with wrangler d1 execute, and query from Workers with a prepared statement API.
Schema and Migrations#
-- schema/0001-init.sql
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- Full-text search with FTS5
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts USING fts5(
id, title, content,
content='articles',
content_rowid='rowid'
);Run migrations:
# Local development
npx wrangler d1 execute my-db --local --file=schema/0001-init.sql
# Production
npx wrangler d1 execute my-db --remote --file=schema/0001-init.sql
# Ad-hoc queries
npx wrangler d1 execute my-db --remote --command "SELECT COUNT(*) FROM articles"Querying from Workers#
// Prepared statements (always use these -- prevents SQL injection)
const article = await env.DB.prepare(
"SELECT * FROM articles WHERE id = ?"
).bind(articleId).first();
// Multiple results
const { results } = await env.DB.prepare(
"SELECT id, title FROM articles WHERE title LIKE ? LIMIT ?"
).bind(`%${query}%`, limit).all();
// Batch operations
const batch = [
env.DB.prepare("INSERT INTO articles (id, title) VALUES (?, ?)").bind("a1", "First"),
env.DB.prepare("INSERT INTO articles (id, title) VALUES (?, ?)").bind("a2", "Second"),
];
await env.DB.batch(batch);D1 Characteristics#
- SQL dialect: SQLite (with some limitations – no
ALTER TABLE DROP COLUMNon older schemas) - Read scaling: Reads are served from read replicas at the edge automatically
- Write model: Single writer in the primary region. Writes are fast but go to one location
- Max database size: 10 GB (paid plan)
- Max rows returned: 100,000 per query
- Transactions: Supported via
db.batch()(implicit transaction) or explicitBEGIN/COMMIT
When D1 Fits#
D1 is right for applications with moderate write throughput and high read volume: content databases, user metadata, config stores, API backends. It is wrong for write-heavy analytics (use Analytics Engine), real-time transactional workloads (use managed Postgres), or datasets exceeding 10 GB.
KV: Edge Key-Value Store#
KV is a globally distributed key-value store optimized for read-heavy workloads. Writes propagate globally within 60 seconds. Reads are served from the nearest edge location with sub-millisecond latency.
Common Patterns#
// Cache with TTL
async function cached<T>(
kv: KVNamespace, key: string, ttlSeconds: number,
handler: () => Promise<T>,
): Promise<T> {
const hit = await kv.get(key, "json");
if (hit) return hit as T;
const result = await handler();
await kv.put(key, JSON.stringify(result), { expirationTtl: ttlSeconds });
return result;
}
// Rate limiting (60 req/min per IP)
async function checkRateLimit(kv: KVNamespace, ip: string): Promise<boolean> {
const key = `rl:${ip}`;
const current = parseInt((await kv.get(key)) || "0", 10);
if (current >= 60) return false; // rate limited
await kv.put(key, String(current + 1), { expirationTtl: 60 });
return true;
}
// Session tokens
await kv.put(`session:${token}`, JSON.stringify(userData), {
expirationTtl: 86400 // 24 hours
});KV Characteristics#
- Value size: 25 MB max per key
- Key size: 512 bytes max
- Consistency: Eventually consistent (writes propagate globally in ~60s)
- Read latency: Sub-millisecond from edge cache
- Best for: Caching, session tokens, rate limiting counters, feature flags, config
- Not for: Frequently updated counters needing strong consistency (use Durable Objects)
R2: Zero-Egress Object Storage#
R2 is S3-compatible object storage with one critical difference: zero egress fees. Downloading data from R2 costs nothing, regardless of volume. This eliminates the most unpredictable line item in cloud storage bills.
// Upload an object
await env.ARTIFACTS.put("reports/2026/q1.pdf", pdfBuffer, {
httpMetadata: { contentType: "application/pdf" },
customMetadata: { generatedBy: "worker-pipeline" },
});
// Download an object
const object = await env.ARTIFACTS.get("reports/2026/q1.pdf");
if (object) {
return new Response(object.body, {
headers: { "Content-Type": object.httpMetadata?.contentType || "application/octet-stream" },
});
}
// List objects with prefix
const listed = await env.ARTIFACTS.list({ prefix: "reports/2026/" });R2 Characteristics#
- API: S3-compatible (works with existing S3 SDKs and tools like
rclone,aws s3) - Egress: $0 – unlimited free downloads
- Storage: $0.015/GB/month (vs S3 $0.023/GB/month)
- Free tier: 10 GB storage, 10M Class A ops (PUT/POST), 10M Class B ops (GET)
- Max object size: 5 TB (multipart upload)
- Best for: Serving assets, cross-cloud staging, artifact storage, public downloads
- Not for: Low-latency random reads of many small objects (use KV), database workloads
Pages: Static Site Hosting#
Pages hosts static sites with git-integrated continuous deployment. Push to a branch and Pages builds and deploys automatically. Every branch gets a preview URL.
# Deploy a Hugo/Next.js/Astro site
# 1. Connect your Git repo in the Cloudflare dashboard
# 2. Set build command (e.g., "hugo" or "npm run build")
# 3. Set output directory (e.g., "public" or "dist")
# 4. Push to main -- Pages builds and deploys in ~30 secondsPages supports Functions – Workers that run on Pages routes. This lets you add server-side logic (API endpoints, form handlers, auth) to a static site without a separate Workers project.
Pages Characteristics#
- Bandwidth: Unlimited on all plans
- Build minutes: 500/month (free), 5,000/month (paid)
- Custom domains: Unlimited, automatic SSL
- Preview deployments: Every branch and PR gets a unique URL
- Rollbacks: Instant rollback to any previous deployment
Durable Objects: Stateful Coordination#
Durable Objects provide stateful, single-threaded serverless execution. Each object has a globally unique ID, its own SQLite-backed storage, and processes requests one at a time. This eliminates race conditions and makes them ideal for coordination problems.
export class SessionManager implements DurableObject {
private state: DurableObjectState;
constructor(state: DurableObjectState) {
this.state = state;
}
async fetch(request: Request): Promise<Response> {
// Single-threaded -- no concurrent access to worry about
const current = await this.state.storage.get<number>("counter") || 0;
await this.state.storage.put("counter", current + 1);
return Response.json({ count: current + 1 });
}
}Use cases: per-user session state, rate limiting with strong consistency, WebSocket coordination (chat rooms, collaborative editing), distributed locks, leader election.
Durable Objects Characteristics#
- Consistency: Strong – single-threaded execution per object
- Storage: SQLite-backed, persistent across requests
- Hibernation: Auto-hibernates when idle (you pay nothing for sleeping objects)
- WebSocket support: Built-in WebSocket handling with hibernatable connections
- Location: Colocated with the client or pinned to a region
Queues: Async Processing#
Queues connect Workers for async, decoupled processing. A producer Worker sends messages; a consumer Worker processes them in batches.
// Producer: send a message
await env.QUEUE.send({
type: "process-upload",
objectKey: "uploads/file.csv",
userId: "user-123",
});
// Consumer: handle messages in batches
export default {
async queue(batch: MessageBatch<QueueMessage>, env: Env): Promise<void> {
for (const message of batch.messages) {
await processMessage(message.body, env);
message.ack();
}
},
};Use cases: background processing (image resize, PDF generation), webhook delivery, import/export pipelines, decoupling write-heavy operations from request-serving Workers.
When NOT to Use Workers#
Workers are not the right choice for every workload:
- Long-running jobs (>30s CPU time): Use traditional servers, containers, or Cloudflare Containers (beta). Workers have a 30-second CPU time limit on the paid plan.
- Heavy compute (ML training, video encoding, large data transforms): Workers have 128 MB memory. Use dedicated compute (EC2, Cloud Run, Fly.io).
- Real-time database needs (high write throughput, multi-writer): D1 is single-writer. Use managed PostgreSQL (Neon, Supabase, RDS) for write-heavy workloads.
- Complex runtime dependencies: Workers run V8 isolates, not containers. If you need specific system libraries, native binaries, or Docker, use a container platform.
- Large response streaming: Workers have limits on response sizes and streaming durations. For long-lived SSE or streaming large files, consider a container-based origin.
Putting It Together: Full-Stack Architecture#
A typical full-stack Cloudflare application combines these services:
┌─────────────────────────────────────────────┐
│ Cloudflare Edge (330+ locations) │
│ │
│ Pages ─── Static site (HTML, JS, CSS) │
│ │ │
│ Worker ── API (routing, auth, business │
│ │ logic, CORS, rate limiting) │
│ ├── D1 ── Relational data (users, │
│ │ content, metadata) │
│ ├── KV ── Cache, sessions, rate limits │
│ ├── R2 ── Files, artifacts, uploads │
│ └── Queue ── Async jobs (email, imports) │
└─────────────────────────────────────────────┘Cost at small scale (< 1M requests/month): $5/month for the Workers paid plan. D1, KV, R2, and Pages stay within free tiers. This runs a production API, database, cache, file storage, and static site for the cost of a coffee.
Cost at medium scale (10M requests/month, 50 GB storage, 10K daily active users): approximately $15-50/month depending on D1 query volume and R2 storage.
This cost structure is possible because Cloudflare does not charge for egress (R2), does not charge for bandwidth (Pages, Workers), and uses a serverless model where you pay per operation rather than per instance-hour.