Generated on: 7/18/2025, 8:22:50 PM
In an era where cloud costs can spiral out of control before your first user signs up, Cloudflare has quietly revolutionized how we think about infrastructure. While AWS charges for every gigabyte transferred and Vercel meters your serverless functions by the millisecond, Cloudflare offers a genuinely production-ready free tier that can serve thousands of users without charging a cent.
This isn't about building toy projects or proof-of-concepts. This is about architecting real applications that scale globally, respond in milliseconds, and handle production workloads—all while your infrastructure bill remains exactly $0.
Most "free tiers" are designed as teasers—just enough to prototype, but never enough to launch. They're carefully calculated to force upgrades the moment you gain traction. Cloudflare took a radically different approach:
- Unlimited bandwidth on static hosting (Cloudflare Pages)
- 100,000 requests per day for serverless functions (Workers)
- No cold starts with V8 isolates that run globally
- Zero egress fees on object storage (R2)
- 10,000 AI inferences daily including LLMs and image generation
- 5 million vectors for semantic search (Vectorize)
As we explore each service, you'll see how they build upon each other:
Foundation → Data Layer → Storage → Intelligence → Production
Pages D1/KV R2 AI/Vectorize Complete
Traditional cloud architecture centers around regions. You pick US-East-1 or EU-West-2 and accept that users on the other side of the world will have a subpar experience. Cloudflare flips this model:
Traditional Cloud:
User → Internet → AWS Region → Database → Response
(200ms+ for distant users)
Cloudflare Edge:
User → Nearest Edge (one of 300+) → Response
(Under 50ms globally)
Your code runs within 50 milliseconds of every human on Earth. Your database lives at the edge. Your AI models inference at the edge. This isn't just about performance—it's about building applications that feel local to everyone, everywhere.
This guide will take you from zero to production, exploring each Cloudflare service and—more importantly—how to combine them into complete systems. You'll discover:
- How to architect applications that cost nothing to run
- When to use each service for maximum efficiency
- Patterns for staying within free tier limits
- Real optimization strategies from production deployments
- How to scale gracefully when success demands it
We'll progressively build increasingly sophisticated applications:
- Static Sites with Dynamic Features: Start with Pages and add API routes
- Full-Stack Applications: Integrate D1 database, KV sessions, and R2 storage
- AI-Powered Platforms: Add vector search and LLM capabilities
- Production Systems: Implement authentication, rate limiting, and monitoring
Each service unlocks new possibilities:
- D1 gives you a real SQL database with 5GB storage
- KV provides lightning-fast key-value storage for sessions
- R2 offers S3-compatible object storage without egress charges
- Vectorize enables semantic search over millions of documents
- Workers AI brings GPT-level models to your applications
- Queues handles async processing and job scheduling
- Analytics Engine tracks custom metrics without privacy concerns
Let's be transparent about what "free" actually means in practice:
const dailyCapacity = {
requests: 100_000, // ~2-10K daily active users
kvReads: 100_000, // Plenty for sessions
d1Reads: 5_000_000, // Essentially unlimited
aiInferences: 10_000, // ~2-3K AI features/day
vectorSearches: 30_000, // Semantic search for all
r2Storage: "10GB", // ~100K images
bandwidth: "Unlimited" // Actually unlimited
}
These aren't trial limits—they're permanent. Many successful applications never need to upgrade.
- Indie Developers tired of cloud bills eating their revenue
- Startups needing global scale without venture funding
- Enterprises exploring edge computing architectures
- Students learning modern web development
- Anyone who believes infrastructure should empower, not impoverish
You'll need:
- Basic JavaScript/TypeScript knowledge
- A Cloudflare account (free)
- Node.js installed locally
- Curiosity about edge computing
Cloudflare's free tier isn't charity—it's a radical bet that democratizing infrastructure creates more value than gatekeeping it. Every request you serve makes their network stronger. Every application you build proves the edge computing model. It's a positive-sum game where everyone wins.
By the end of this guide, you'll have the knowledge to build applications that would cost hundreds or thousands monthly on traditional clouds, running entirely for free on Cloudflare. More importantly, you'll understand a new paradigm of computing—one where the edge isn't an optimization, but the foundation.
Ready to build your $0 infrastructure stack? Let's start with the foundation of any web application.
Next: Cloudflare Pages - Your zero-cost global CDN
Before we dive into building on Cloudflare's free tier, let's have an honest conversation about the trade-offs. While the $0 infrastructure stack is powerful and genuinely production-ready for many use cases, it's not a silver bullet. Understanding these limitations upfront will help you make informed decisions and set realistic expectations.
When you build on the $0 stack, you're making a significant commitment to Cloudflare's ecosystem. This isn't like choosing between AWS and GCP where many services have direct equivalents.
// Traditional cloud - relatively portable
const db = new AWS.DynamoDB() // → Can migrate to GCP Firestore
const storage = new AWS.S3() // → Can migrate to GCP Storage
// Cloudflare - deeply integrated
const db = env.DB // D1 SQL syntax, but edge-native
const kv = env.KV // No direct equivalent elsewhere
const ai = env.AI // Cloudflare-specific models
Migration challenges:
- Workers → Porting to AWS Lambda requires significant refactoring (different runtime, no edge deployment)
- D1 → Moving to PostgreSQL means losing edge proximity and dealing with connection pooling
- KV → No direct equivalent; would need Redis or DynamoDB with different access patterns
- Durable Objects → Completely unique; would require architectural redesign
Mitigation strategies:
- Keep business logic separate from Cloudflare-specific APIs
- Use repository pattern for data access
- Consider hybrid architectures for critical components
Building on the edge isn't just "serverless in more locations." It requires fundamental shifts in how you architect applications.
// Traditional approach - strong consistency
await db.transaction(async (trx) => {
await trx.update(users).set({ balance: 100 })
await trx.insert(ledger).values({ amount: -50 })
})
// Edge approach - embrace eventual consistency
await env.KV.put(`user:${id}`, JSON.stringify({ balance: 100 }))
// Balance might not be immediately consistent across all edges
Workers have strict CPU limits (10-50ms). This rules out:
- Video encoding/transcoding
- Large file processing
- Complex ML model training
- Long-polling connections
// Challenge: Global counters, leaderboards, real-time collaboration
// Solution: Durable Objects, but they add complexity
export class Counter {
constructor(state, env) {
this.state = state
}
async fetch(request) {
// All requests for this counter must route to one location
// Adds latency for global users
}
}
Let's be explicit about scenarios where AWS, GCP, or Azure might serve you better:
Use traditional cloud for:
- Video processing pipelines
- Large-scale data analysis
- ML model training (not inference)
- Batch processing jobs > 30 seconds
Example: A video platform like YouTube or TikTok needs dedicated compute for transcoding. Workers' 50ms CPU limit makes this impossible at the edge.
Use traditional cloud for:
- Multi-region strong consistency requirements
- Complex transactions across many tables
- Real-time financial systems
- Healthcare systems with strict ACID requirements
Example: A banking system with complex transaction requirements would struggle with D1's eventual consistency model.
Use traditional cloud for:
- Time-series databases (InfluxDB, TimescaleDB)
- Graph databases (Neo4j, Amazon Neptune)
- Full-text search (Elasticsearch)
- Geospatial queries (PostGIS)
Example: A social network needing complex graph traversals would require Neo4j or similar, which Cloudflare doesn't offer.
Use traditional cloud for:
- On-premise database connections
- VPN requirements
- Legacy protocol support (SOAP, etc.)
- Windows-based applications
Use traditional cloud for:
- Specific data residency requirements
- Industries requiring FedRAMP, HIPAA specifics
- Government contracts with cloud restrictions
Be prepared for these paradigm shifts:
- Connection pooling doesn't exist - Each request is isolated
- No local file system - Everything must be in object storage or memory
- Different debugging - No SSH, limited logging, distributed traces
- New patterns - Event-driven, not request-driven architectures
The $0 stack is excellent for:
- ✅ API backends and microservices
- ✅ Static sites with dynamic elements
- ✅ Real-time applications (with Durable Objects)
- ✅ Content-heavy applications
- ✅ Global SaaS products
- ✅ JAMstack applications
Consider alternatives for:
- ❌ Heavy batch processing
- ❌ Complex ML training
- ❌ Legacy system integration
- ❌ Applications requiring specific databases
- ❌ Monolithic architectures
The Cloudflare free tier isn't about doing everything for free—it's about doing the right things exceptionally well for free. If your application fits the edge paradigm, you'll get better performance and lower costs than traditional cloud. If it doesn't, forcing it will lead to frustration.
The key is understanding these trade-offs before you build, not after. In the following chapters, we'll show you how to leverage the platform's strengths while working within its constraints. And for many modern applications, those constraints won't matter—the benefits will far outweigh them.
Remember: Even tech giants like Discord, Shopify, and DoorDash use Cloudflare for significant portions of their infrastructure. The platform is production-ready; the question is whether your specific use case aligns with its strengths.
The Pitch: Cloudflare Pages hosts your static sites on a global CDN with truly unlimited bandwidth. While competitors charge $180/month for 1TB of traffic, Pages charges $0—forever.
The Win: Deploy a site globally in under 60 seconds:
# Create a simple site mkdir my-site && cd my-site echo '<h1>Hello from the edge!</h1>' > index.html # Deploy to Cloudflare's global network npx wrangler pages deploy . --project-name=my-first-site # Your site is now live at: # https://my-first-site.pages.dev # 👆 Zero config. Zero cost. Deployed to 300+ locations.The Catch: The 500 builds/month limit means you'll need to be thoughtful about CI/CD pipelines. Direct uploads (like above) don't count against this limit.
TL;DR - Key Takeaways
- What: Static hosting with unlimited bandwidth and global CDN
- Free Tier: Unlimited sites, unlimited requests, unlimited bandwidth
- Primary Use Cases: Static sites, SPAs, JAMstack apps, documentation
- Key Features: Git integration, preview deployments, custom domains, edge functions
- Limitations: 500 builds/month, 100MB per file, 25MB total per deployment
Cloudflare Pages isn't just static hosting—it's your gateway to the entire Cloudflare ecosystem. With truly unlimited bandwidth and global distribution included free, it's the perfect starting point for any project.
Traditional static hosts nickel-and-dime you on bandwidth. Vercel gives you 100GB/month free, Netlify offers 100GB/month, AWS CloudFront... well, check your credit card. Cloudflare Pages? Unlimited. Not "unlimited with fair use policy"—actually unlimited.
// Bandwidth cost comparison (per month)
const bandwidthCosts = {
vercel: {
included: "100GB",
overage: "$0.15/GB",
example1TB: (1000 - 100) * 0.15 // $135/month
},
netlify: {
included: "100GB",
overage: "$0.20/GB",
example1TB: (1000 - 100) * 0.20 // $180/month
},
aws: {
included: "0GB",
overage: "$0.085/GB",
example1TB: 1000 * 0.085 // $85/month
},
cloudflare: {
included: "Unlimited",
overage: "$0",
example1TB: 0 // $0/month
}
}
# Install Wrangler CLI
npm install -g wrangler
# Create a new Pages project
npm create cloudflare@latest my-app -- --framework=react
# Deploy instantly
npx wrangler pages deploy dist
Your site is now live at https://my-app.pages.dev
and distributed to 300+ global locations.
Pages seamlessly integrates with Workers for dynamic functionality:
// functions/api/hello.ts
export async function onRequest(context) {
return new Response(JSON.stringify({
message: "Hello from the edge!",
location: context.cf.colo
}))
}
This function runs at the edge, no configuration needed. Access it at /api/hello
.
# Add your domain
wrangler pages domain add example.com
# Automatic SSL, global anycast, DDoS protection included
Every git branch gets a unique URL:
main
→my-app.pages.dev
feature
→feature.my-app.pages.dev
- PR #123 →
123.my-app.pages.dev
// wrangler.toml
{
"build": {
"command": "npm run build",
"output": "dist"
},
"env": {
"production": {
"vars": {
"API_URL": "https://api.example.com"
}
}
}
}
The real magic happens when you combine Pages with Workers:
// functions/api/[[catchall]].ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/users', async (c) => {
const users = await c.env.DB.prepare('SELECT * FROM users').all()
return c.json(users)
})
export async function onRequest(context) {
return app.fetch(context.request, context.env)
}
Now your static site has a full API backend, running at the edge.
my-app/
├── dist/ # React/Vue/Svelte build
├── functions/ # Edge API routes
│ ├── api/
│ │ ├── auth.ts # Authentication endpoints
│ │ ├── data.ts # CRUD operations
│ │ └── [[catchall]].ts # Hono router
└── wrangler.toml
// functions/contact.ts
export async function onRequestPost(context) {
const formData = await context.request.formData()
// Send email via Email routing
await context.env.EMAIL.send({
to: '[email protected]',
from: '[email protected]',
subject: 'Contact Form',
text: formData.get('message')
})
return Response.redirect('/thank-you')
}
// functions/images/[[path]].ts
export async function onRequest(context) {
const url = new URL(context.request.url)
const width = url.searchParams.get('w') || '800'
// Fetch from R2
const original = await context.env.IMAGES.get(context.params.path)
// Transform with Cloudflare Images
return fetch(`/cdn-cgi/image/width=${width}/${original.url}`)
}
// functions/_middleware.ts
export async function onRequest(context) {
const response = await context.next()
// Cache static API responses
if (context.request.url.includes('/api/static/')) {
response.headers.set('Cache-Control', 'public, max-age=3600')
}
return response
}
// build config
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'axios']
}
}
}
}
}
Pages includes built-in analytics:
// Track custom events
export async function onRequest(context) {
// Log to Analytics Engine
context.env.ANALYTICS.writeDataPoint({
dataset: 'pages_metrics',
point: {
url: context.request.url,
method: context.request.method,
timestamp: Date.now()
}
})
return context.next()
}
Let's be concrete about the savings:
// Medium-traffic site: 50GB/day bandwidth
const monthlyCosts = {
aws: 50 * 30 * 0.085, // $127.50/month
vercel: (1500 - 100) * 0.15, // $210/month
cloudflare: 0 // $0/month
}
// High-traffic site: 500GB/day
const highTrafficCosts = {
aws: 500 * 30 * 0.085, // $1,275/month
vercel: (15000 - 100) * 0.15,// $2,235/month
cloudflare: 0 // Still $0/month
}
Perfect for:
- Marketing sites and landing pages
- Documentation sites
- SPAs with API backends
- Blogs and content sites
- E-commerce frontends
- Portfolio sites
Consider alternatives when:
- You need complex server-side rendering (use Workers)
- You require long-running processes (use Durable Objects)
- You need WebSocket servers (use Durable Objects)
- Optimize Assets: Use modern formats (WebP, AVIF)
- Enable Compression: Brotli compression is automatic
- Use HTTP/3: Enabled by default for all sites
- Implement Caching: Set proper cache headers
- Monitor Performance: Use Web Analytics (also free)
// functions/blog/[slug].ts
export async function onRequest(context) {
const cache = caches.default
const cacheKey = new Request(context.request.url)
// Try cache first
let response = await cache.match(cacheKey)
if (!response) {
// Generate page
response = await generateBlogPost(context.params.slug)
// Cache for 1 hour
response.headers.set('Cache-Control', 'public, max-age=3600')
context.waitUntil(cache.put(cacheKey, response.clone()))
}
return response
}
// functions/_middleware.ts
export async function onRequest(context) {
const cookie = context.request.headers.get('Cookie')
const variant = cookie?.includes('variant=B') ? 'B' : 'A'
// Serve different content
if (variant === 'B' && context.request.url.includes('index.html')) {
return fetch('https://my-app.pages.dev/index-b.html')
}
return context.next()
}
Pages is your entry point to:
- Workers: Add API endpoints
- KV: Store user sessions
- D1: Add a database
- R2: Handle file uploads
- Email: Process contact forms
- Analytics: Track usage
Cloudflare Pages redefines what free hosting means. It's not a limited trial or a loss leader—it's a production-ready platform that happens to cost nothing. With unlimited bandwidth, global distribution, and seamless integration, Pages provides everything needed for modern web hosting: CDN, SSL, DDoS protection, and global performance.
Next: Workers - Adding serverless compute to your Pages site
The Pitch: Workers runs your JavaScript/TypeScript code in 300+ locations worldwide with zero cold starts. Unlike AWS Lambda's regional deployments, your API responds in milliseconds from everywhere.
The Win: Create and deploy a global API in 2 minutes:
// worker.js - A complete API that runs globally export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/api/hello") { return Response.json({ message: "Hello from the edge!", location: request.cf?.city || "Unknown", latency: "< 50ms worldwide" }); } return new Response("Not found", { status: 404 }); } }; // Deploy: npx wrangler deploy worker.js // 👆 Your API is now live globally with 100K free requests/dayThe Catch: The 10ms CPU limit means Workers are best for I/O operations, not heavy computation. Think API routing, not video encoding.
TL;DR - Key Takeaways
- What: Serverless functions running in 300+ global locations
- Free Tier: 100,000 requests/day, 10ms CPU/request
- Primary Use Cases: APIs, dynamic routing, authentication, real-time features
- Key Features: Zero cold starts, global deployment, KV/D1/R2 bindings
- Limitations: 10ms CPU burst (50ms sustained), 128MB memory, no persistent connections
Workers fundamentally changes how we think about backend infrastructure. Instead of managing servers in specific regions, your code runs within 50ms of every user on Earth. With 100,000 free requests daily and no cold starts, it's serverless computing as it should be.
Traditional serverless platforms suffer from fundamental flaws:
// Serverless platform comparison
const serverlessComparison = {
awsLambda: {
coldStart: "500ms-3s first request",
region: "Pick one, far users suffer",
complexity: "VPCs, security groups, IAM",
cost: "$0.20/million requests + compute"
},
cloudflareWorkers: {
coldStart: "0ms - always warm",
region: "All 300+ locations simultaneously",
complexity: "Just deploy your code",
cost: "100K requests/day free"
}
}
// src/index.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url)
if (url.pathname === '/api/hello') {
return new Response(JSON.stringify({
message: 'Hello from the edge!',
location: request.cf?.city,
timestamp: Date.now()
}), {
headers: { 'Content-Type': 'application/json' }
})
}
return new Response('Not Found', { status: 404 })
}
}
Deploy instantly:
wrangler deploy
# ⛅️ Deployed to https://my-worker.username.workers.dev
Workers run in V8 isolates, not containers:
Traditional Serverless:
Request → Load Balancer → Cold Container Startup (500ms+) → Your Code
Workers:
Request → Nearest Edge → Existing V8 Isolate (0ms) → Your Code
This architecture enables:
- Zero cold starts: Isolates are always ready
- Minimal overhead: ~5ms baseline latency
- Global deployment: Same code runs everywhere
- Automatic scaling: Handles traffic spikes instantly
Workers becomes powerful when integrated with other services:
interface Env {
DB: D1Database // SQL database
KV: KVNamespace // Key-value store
BUCKET: R2Bucket // Object storage
QUEUE: Queue // Message queue
AI: Ai // AI models
VECTORIZE: VectorizeIndex // Vector search
}
export default {
async fetch(request: Request, env: Env) {
// Query database
const users = await env.DB
.prepare('SELECT * FROM users WHERE active = ?')
.bind(true)
.all()
// Cache in KV
await env.KV.put('active_users', JSON.stringify(users), {
expirationTtl: 3600
})
// Store files in R2
const file = await request.blob()
await env.BUCKET.put(`uploads/${Date.now()}`, file)
// Queue background job
await env.QUEUE.send({
type: 'process_upload',
timestamp: Date.now()
})
return Response.json({ success: true })
}
}
import { Router } from 'itty-router'
const router = Router()
// User endpoints
router.get('/api/users', async (request, env) => {
const users = await env.DB.prepare('SELECT * FROM users').all()
return Response.json(users.results)
})
router.post('/api/users', async (request, env) => {
const user = await request.json()
const result = await env.DB
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.bind(user.name, user.email)
.run()
return Response.json({ id: result.meta.last_row_id })
})
router.get('/api/users/:id', async (request, env) => {
const user = await env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(request.params.id)
.first()
return user
? Response.json(user)
: new Response('Not Found', { status: 404 })
})
// Handle all requests
export default {
fetch: router.handle
}
async function authenticate(request: Request, env: Env) {
const token = request.headers.get('Authorization')?.split(' ')[1]
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
try {
const payload = await jwt.verify(token, env.JWT_SECRET)
request.user = payload
} catch (error) {
return new Response('Invalid token', { status: 401 })
}
}
router.get('/api/protected/*', authenticate, async (request) => {
// User is authenticated
return Response.json({
message: 'Secret data',
user: request.user
})
})
async function rateLimit(request: Request, env: Env) {
const ip = request.headers.get('CF-Connecting-IP')
const key = `rate_limit:${ip}`
const current = await env.KV.get(key)
const count = current ? parseInt(current) + 1 : 1
if (count > 100) { // 100 requests per hour
return new Response('Rate limit exceeded', { status: 429 })
}
await env.KV.put(key, count.toString(), {
expirationTtl: 3600
})
}
router.all('*', rateLimit)
// wrangler.toml
// [triggers]
// crons = ["0 * * * *"] # Every hour
export default {
async scheduled(event: ScheduledEvent, env: Env) {
// Clean up old sessions
const yesterday = Date.now() - 86400000
await env.DB
.prepare('DELETE FROM sessions WHERE created_at < ?')
.bind(yesterday)
.run()
// Generate reports
const stats = await calculateDailyStats(env)
await env.KV.put('daily_stats', JSON.stringify(stats))
}
}
export default {
async fetch(request: Request, env: Env) {
if (request.headers.get('Upgrade') === 'websocket') {
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
handleWebSocket(server, env)
return new Response(null, {
status: 101,
webSocket: client
})
}
return new Response('Expected WebSocket', { status: 400 })
}
}
async function handleWebSocket(ws: WebSocket, env: Env) {
ws.accept()
ws.addEventListener('message', async (event) => {
const data = JSON.parse(event.data)
// Broadcast to all connected clients
await env.CONNECTIONS.broadcast(data)
})
}
// worker-a/wrangler.toml
// [[services]]
// binding = "AUTH_SERVICE"
// service = "auth-worker"
export default {
async fetch(request: Request, env: Env) {
// Call another Worker directly (no HTTP overhead)
const authResult = await env.AUTH_SERVICE.fetch(
new Request('http://internal/verify', {
method: 'POST',
body: JSON.stringify({ token: request.headers.get('Authorization') })
})
)
if (!authResult.ok) {
return new Response('Unauthorized', { status: 401 })
}
// Continue with authenticated request
return handleRequest(request, env)
}
}
const cache = new Map()
async function fetchWithCoalescing(key: string, fetcher: () => Promise<any>) {
if (cache.has(key)) {
return cache.get(key)
}
const promise = fetcher()
cache.set(key, promise)
try {
const result = await promise
setTimeout(() => cache.delete(key), 5000) // Cache for 5 seconds
return result
} catch (error) {
cache.delete(key)
throw error
}
}
export default {
async fetch(request: Request, env: Env) {
const data = await fetchWithCoalescing('users', async () => {
return env.DB.prepare('SELECT * FROM users').all()
})
return Response.json(data)
}
}
export default {
async fetch(request: Request, env: Env) {
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
// Start streaming immediately
const response = new Response(readable, {
headers: { 'Content-Type': 'text/event-stream' }
})
// Process in background
const encoder = new TextEncoder()
env.ctx.waitUntil(
(async () => {
for (let i = 0; i < 100; i++) {
await writer.write(encoder.encode(`data: Event ${i}\n\n`))
await new Promise(resolve => setTimeout(resolve, 100))
}
writer.close()
})()
)
return response
}
}
Understanding the free tier limits:
const freeTierLimits = {
requests: 100_000, // Per day
cpuTime: 10, // Milliseconds per request
memory: 128, // MB per request
subrequests: 50, // External fetches per request
envVars: 64, // Environment variables
scriptSize: 1, // MB after compression
}
// Optimization strategies
const optimizations = {
caching: "Use Cache API and KV for repeated data",
batching: "Combine multiple operations",
async: "Use waitUntil() for non-critical tasks",
compression: "Compress large responses",
earlyReturns: "Return responses as soon as possible"
}
// pages/functions/api/[[path]].ts
export { default } from 'my-worker'
const schema = `
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_created ON articles(created_at);
`
export default {
async fetch(request: Request, env: Env) {
// Initialize database
await env.DB.exec(schema)
// Efficient queries
const recent = await env.DB
.prepare('SELECT * FROM articles ORDER BY created_at DESC LIMIT ?')
.bind(10)
.all()
return Response.json(recent.results)
}
}
export default {
async fetch(request: Request, env: Env) {
if (request.method === 'PUT') {
const key = new URL(request.url).pathname.slice(1)
await env.BUCKET.put(key, request.body)
return Response.json({
uploaded: key,
size: request.headers.get('Content-Length')
})
}
// Generate presigned URL
const url = await env.BUCKET.createSignedUrl(key, {
expiresIn: 3600
})
return Response.json({ url })
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const startTime = Date.now()
try {
const response = await handleRequest(request, env)
// Log to Analytics Engine
ctx.waitUntil(
env.ANALYTICS.writeDataPoint({
dataset: 'api_metrics',
point: {
route: new URL(request.url).pathname,
method: request.method,
status: response.status,
duration: Date.now() - startTime,
timestamp: Date.now()
}
})
)
return response
} catch (error) {
// Log errors
console.error('Request failed:', error)
return new Response('Internal Error', { status: 500 })
}
}
}
Real-world cost analysis:
// 3 million requests/month (100K/day)
const monthlyCosts = {
awsLambda: {
requests: 3_000_000 * 0.0000002, // $0.60
compute: 3_000_000 * 0.0000166667, // $50.00
total: 50.60 // Plus API Gateway, etc.
},
vercelFunctions: {
included: 1_000_000, // Free tier
additional: 2_000_000 * 0.00001, // $20.00
total: 20.00
},
cloudflareWorkers: {
total: 0 // 100K/day = 3M/month free
}
}
Perfect for:
- API backends
- Authentication services
- Image/video processing
- Real-time features
- Webhook handlers
- Edge middleware
Consider alternatives when:
- Need > 10ms CPU time (use Durable Objects)
- Require persistent connections (use Durable Objects)
- Need to run containers (use traditional cloud)
Workers represents a paradigm shift in serverless computing. By running at the edge with zero cold starts and true global distribution, it enables applications that were previously impossible or prohibitively expensive. The generous free tier—100,000 requests daily—is enough for most applications to run indefinitely without cost. Workers excels at APIs, webhooks, real-time features, and any compute workload that benefits from global distribution.
Next: D1 Database - SQLite at the edge
The Pitch: Hono is a 12KB web framework that brings Express-like simplicity to Cloudflare Workers with zero cold starts and full TypeScript support.
The Win:
import { Hono } from 'hono' const app = new Hono() app.get('/api/hello/:name', (c) => c.json({ message: `Hello ${c.req.param('name')}!` }) ) export default appThe Catch: No built-in database ORM or file system access - you'll need to integrate with Cloudflare's services (D1, KV, R2) for persistence.
TL;DR - Key Takeaways
- What: Ultrafast web framework designed for edge computing
- Free Tier: Framework itself is free (uses Workers' limits)
- Primary Use Cases: Building REST APIs, handling routing, middleware
- Key Features: 12KB size, TypeScript-first, Express-like API, zero dependencies
- Why Use: Makes Workers development 10x more productive and maintainable
Hono is a small, fast web framework designed specifically for edge computing. With zero dependencies, sub-millisecond overhead, and a familiar Express-like API, it's the ideal framework for building APIs on Cloudflare Workers.
// Web framework comparison
const frameworkComparison = {
express: {
size: "2.5MB+ with dependencies",
coldStart: "200-500ms",
routing: "Regex-based (slower)",
typescript: "Needs configuration",
edge: "Not optimized for edge"
},
hono: {
size: "12KB (200x smaller)",
coldStart: "0ms on Workers",
routing: "Trie-based (3x faster)",
typescript: "First-class support",
edge: "Built for Workers, Deno, Bun"
}
}
# Create new Hono app on Cloudflare
npm create hono@latest my-app
# Select "cloudflare-workers" template
# Or add to existing Worker
npm install hono
Basic Hono application:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
// Type-safe environment bindings
type Bindings = {
DB: D1Database
KV: KVNamespace
BUCKET: R2Bucket
AI: Ai
}
const app = new Hono<{ Bindings: Bindings }>()
// Middleware
app.use('*', logger())
app.use('/api/*', cors())
// Routes
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.get('/api/users/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first()
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(user)
})
export default app
import { Hono } from 'hono'
import { validator } from 'hono/validator'
import { z } from 'zod'
const app = new Hono<{ Bindings: Bindings }>()
// Validation schemas
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['user', 'admin']).default('user')
})
const UpdateUserSchema = CreateUserSchema.partial()
// User routes
const users = new Hono<{ Bindings: Bindings }>()
users.get('/', async (c) => {
const { page = '1', limit = '10', search } = c.req.query()
let query = 'SELECT * FROM users WHERE 1=1'
const params: any[] = []
if (search) {
query += ' AND (name LIKE ? OR email LIKE ?)'
params.push(`%${search}%`, `%${search}%`)
}
query += ' LIMIT ? OFFSET ?'
params.push(parseInt(limit), (parseInt(page) - 1) * parseInt(limit))
const result = await c.env.DB
.prepare(query)
.bind(...params)
.all()
const total = await c.env.DB
.prepare('SELECT COUNT(*) as count FROM users')
.first()
return c.json({
users: result.results,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: total?.count || 0,
pages: Math.ceil((total?.count || 0) / parseInt(limit))
}
})
})
users.post('/',
validator('json', (value, c) => {
const parsed = CreateUserSchema.safeParse(value)
if (!parsed.success) {
return c.json({ errors: parsed.error.flatten() }, 400)
}
return parsed.data
}),
async (c) => {
const data = c.req.valid('json')
try {
const result = await c.env.DB
.prepare('INSERT INTO users (email, name, role) VALUES (?, ?, ?)')
.bind(data.email, data.name, data.role)
.run()
const user = await c.env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(result.meta.last_row_id)
.first()
return c.json(user, 201)
} catch (error: any) {
if (error.message.includes('UNIQUE constraint')) {
return c.json({ error: 'Email already exists' }, 409)
}
throw error
}
}
)
users.get('/:id', async (c) => {
const id = c.req.param('id')
const user = await c.env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first()
if (!user) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(user)
})
users.patch('/:id',
validator('json', (value, c) => {
const parsed = UpdateUserSchema.safeParse(value)
if (!parsed.success) {
return c.json({ errors: parsed.error.flatten() }, 400)
}
return parsed.data
}),
async (c) => {
const id = c.req.param('id')
const updates = c.req.valid('json')
// Build dynamic update query
const fields = Object.keys(updates)
const values = Object.values(updates)
if (fields.length === 0) {
return c.json({ error: 'No fields to update' }, 400)
}
const setClause = fields.map(f => `${f} = ?`).join(', ')
const result = await c.env.DB
.prepare(`UPDATE users SET ${setClause} WHERE id = ?`)
.bind(...values, id)
.run()
if (result.meta.changes === 0) {
return c.json({ error: 'User not found' }, 404)
}
const updated = await c.env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(id)
.first()
return c.json(updated)
}
)
users.delete('/:id', async (c) => {
const id = c.req.param('id')
const result = await c.env.DB
.prepare('DELETE FROM users WHERE id = ?')
.bind(id)
.run()
if (result.meta.changes === 0) {
return c.json({ error: 'User not found' }, 404)
}
return c.json({ message: 'User deleted' })
})
// Mount routes
app.route('/api/users', users)
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { HTTPException } from 'hono/http-exception'
// Auth middleware
const auth = new Hono<{ Bindings: Bindings }>()
// Public routes
auth.post('/login', async (c) => {
const { email, password } = await c.req.json()
// Verify credentials
const user = await c.env.DB
.prepare('SELECT * FROM users WHERE email = ?')
.bind(email)
.first()
if (!user || !await verifyPassword(password, user.password_hash)) {
return c.json({ error: 'Invalid credentials' }, 401)
}
// Generate JWT
const token = await sign({
sub: user.id,
email: user.email,
role: user.role,
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24) // 24 hours
}, c.env.JWT_SECRET)
// Store session in KV
await c.env.KV.put(
`session:${token}`,
JSON.stringify({ userId: user.id, email: user.email }),
{ expirationTtl: 86400 }
)
return c.json({ token, user: { id: user.id, email: user.email, role: user.role } })
})
auth.post('/logout',
jwt({ secret: c => c.env.JWT_SECRET }),
async (c) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token) {
await c.env.KV.delete(`session:${token}`)
}
return c.json({ message: 'Logged out' })
}
)
// Role-based access control
function requireRole(...roles: string[]) {
return async (c: any, next: any) => {
const payload = c.get('jwtPayload')
if (!roles.includes(payload.role)) {
throw new HTTPException(403, { message: 'Insufficient permissions' })
}
await next()
}
}
// Protected routes
const admin = new Hono<{ Bindings: Bindings }>()
.use('*', jwt({ secret: c => c.env.JWT_SECRET }))
.use('*', requireRole('admin'))
admin.get('/stats', async (c) => {
const stats = await c.env.DB.prepare(`
SELECT
COUNT(*) as total_users,
COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_users,
COUNT(CASE WHEN created_at > datetime('now', '-7 days') THEN 1 END) as new_users
FROM users
`).first()
return c.json(stats)
})
// Mount auth routes
app.route('/auth', auth)
app.route('/admin', admin)
import { Hono } from 'hono'
const uploads = new Hono<{ Bindings: Bindings }>()
uploads.post('/images', async (c) => {
const formData = await c.req.formData()
const file = formData.get('image') as File
if (!file) {
return c.json({ error: 'No file uploaded' }, 400)
}
// Validate file
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
const maxSize = 10 * 1024 * 1024 // 10MB
if (!allowedTypes.includes(file.type)) {
return c.json({ error: 'Invalid file type' }, 400)
}
if (file.size > maxSize) {
return c.json({ error: 'File too large' }, 400)
}
// Generate unique filename
const ext = file.name.split('.').pop()
const filename = `${crypto.randomUUID()}.${ext}`
const key = `uploads/${new Date().getFullYear()}/${filename}`
// Upload to R2
await c.env.BUCKET.put(key, file, {
httpMetadata: {
contentType: file.type,
},
customMetadata: {
originalName: file.name,
uploadedBy: c.get('jwtPayload')?.sub || 'anonymous',
uploadedAt: new Date().toISOString()
}
})
// Generate thumbnail
c.executionCtx.waitUntil(
generateThumbnail(c.env, key, file)
)
return c.json({
url: `/files/${key}`,
key,
size: file.size,
type: file.type
})
})
async function generateThumbnail(env: Bindings, key: string, file: File) {
// Use Cloudflare Image Resizing
const response = await fetch(`/cdn-cgi/image/width=200,height=200,fit=cover/${key}`)
const thumbnail = await response.blob()
const thumbnailKey = key.replace('uploads/', 'thumbnails/')
await env.BUCKET.put(thumbnailKey, thumbnail)
}
// Serve files with caching
uploads.get('/files/*', async (c) => {
const key = c.req.path.replace('/files/', '')
// Check cache
const cache = caches.default
const cacheKey = new Request(c.req.url)
const cached = await cache.match(cacheKey)
if (cached) {
return cached
}
// Get from R2
const object = await c.env.BUCKET.get(key)
if (!object) {
return c.json({ error: 'File not found' }, 404)
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
headers.set('ETag', object.httpEtag)
const response = new Response(object.body, { headers })
// Cache response
c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()))
return response
})
app.route('/api/uploads', uploads)
import { Hono } from 'hono'
import { upgradeWebSocket } from 'hono/cloudflare-workers'
const ws = new Hono<{ Bindings: Bindings }>()
ws.get('/chat/:room',
upgradeWebSocket((c) => {
const room = c.req.param('room')
const connections = new Set<WebSocket>()
return {
onOpen(event, ws) {
connections.add(ws)
ws.send(JSON.stringify({
type: 'system',
message: `Welcome to room ${room}`
}))
},
onMessage(event, ws) {
const data = JSON.parse(event.data)
// Broadcast to all connections
const message = JSON.stringify({
type: 'message',
user: data.user,
text: data.text,
timestamp: Date.now()
})
for (const conn of connections) {
if (conn.readyState === WebSocket.OPEN) {
conn.send(message)
}
}
},
onClose(event, ws) {
connections.delete(ws)
}
}
})
)
app.route('/ws', ws)
import { Hono } from 'hono'
import { timing } from 'hono/timing'
import { compress } from 'hono/compress'
import { cache } from 'hono/cache'
import { etag } from 'hono/etag'
import { secureHeaders } from 'hono/secure-headers'
// Custom middleware
async function rateLimiter(c: any, next: any) {
const ip = c.req.header('CF-Connecting-IP') || 'unknown'
const key = `rate:${ip}`
const count = parseInt(await c.env.KV.get(key) || '0')
if (count > 100) {
return c.json({ error: 'Rate limit exceeded' }, 429)
}
await c.env.KV.put(key, (count + 1).toString(), {
expirationTtl: 3600
})
await next()
}
// Apply middleware
app.use('*', timing())
app.use('*', secureHeaders())
app.use('*', compress())
app.use('/api/*', rateLimiter)
// Cache GET requests
app.use(
'/api/*',
cache({
cacheName: 'api-cache',
cacheControl: 'max-age=3600',
wait: true,
keyGenerator: (c) => {
const url = new URL(c.req.url)
return url.pathname + url.search
}
})
)
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
// Global error handler
app.onError((err, c) => {
console.error(`${err}`)
if (err instanceof HTTPException) {
return err.getResponse()
}
// Log to external service
c.executionCtx.waitUntil(
logError(c.env, {
error: err.message,
stack: err.stack,
url: c.req.url,
method: c.req.method,
timestamp: Date.now()
})
)
return c.json({
error: 'Internal Server Error',
message: c.env.DEBUG ? err.message : undefined
}, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({
error: 'Not Found',
path: c.req.path
}, 404)
})
// Custom exceptions
class ValidationException extends HTTPException {
constructor(errors: any) {
super(400, { message: 'Validation failed' })
this.errors = errors
}
getResponse() {
return Response.json({
error: 'Validation Error',
errors: this.errors
}, 400)
}
}
// Type-safe params
app.get('/users/:id{[0-9]+}', async (c) => {
const id = parseInt(c.req.param('id'))
// id is guaranteed to be a number
})
// Query string parsing
app.get('/search', async (c) => {
const { q, page = '1', limit = '10', sort = 'relevance' } = c.req.query()
// Array query params
const tags = c.req.queries('tag') || []
// ?tag=javascript&tag=typescript → ['javascript', 'typescript']
})
// Header manipulation
app.use('*', async (c, next) => {
// Request headers
const auth = c.req.header('Authorization')
const contentType = c.req.header('Content-Type')
await next()
// Response headers
c.header('X-Response-Time', `${Date.now() - c.get('startTime')}ms`)
c.header('X-Powered-By', 'Hono/Cloudflare')
})
// Content negotiation
app.get('/data', (c) => {
const accept = c.req.header('Accept')
if (accept?.includes('application/xml')) {
c.header('Content-Type', 'application/xml')
return c.body('<data>...</data>')
}
return c.json({ data: '...' })
})
import { describe, it, expect } from 'vitest'
import app from './app'
describe('API Tests', () => {
it('GET /api/users', async () => {
const res = await app.request('/api/users')
expect(res.status).toBe(200)
const data = await res.json()
expect(data).toHaveProperty('users')
expect(Array.isArray(data.users)).toBe(true)
})
it('POST /api/users', async () => {
const res = await app.request('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
name: 'Test User'
})
})
expect(res.status).toBe(201)
const user = await res.json()
expect(user.email).toBe('[email protected]')
})
it('Rate limiting', async () => {
// Make 101 requests
for (let i = 0; i < 101; i++) {
await app.request('/api/users')
}
const res = await app.request('/api/users')
expect(res.status).toBe(429)
})
})
app.get('/stream', (c) => {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 100; i++) {
controller.enqueue(
encoder.encode(`data: Event ${i}\n\n`)
)
await new Promise(r => setTimeout(r, 100))
}
controller.close()
}
})
return c.newResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
})
app.post('/api/batch', async (c) => {
const operations = await c.req.json()
const results = await Promise.allSettled(
operations.map((op: any) =>
processOperation(op, c.env)
)
)
return c.json({
results: results.map((result, index) => ({
id: operations[index].id,
status: result.status,
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason.message : null
}))
})
})
const honoBestPractices = {
structure: {
routes: "Group related routes in separate Hono instances",
middleware: "Apply middleware selectively, not globally",
validation: "Use zod for runtime type safety",
errors: "Implement consistent error handling"
},
performance: {
routing: "Use specific paths over wildcards",
middleware: "Order middleware by frequency of use",
responses: "Stream large responses",
caching: "Cache at edge when possible"
},
security: {
cors: "Configure CORS appropriately",
headers: "Use secure-headers middleware",
validation: "Validate all inputs",
auth: "Implement proper JWT verification"
},
testing: {
unit: "Test routes with app.request()",
integration: "Test with real bindings",
types: "Leverage TypeScript for safety"
}
}
// Performance comparison (ops/sec)
const benchmarks = {
hono: 196000,
express: 82000,
fastify: 126000,
koa: 91000
}
// Bundle size comparison
const bundleSize = {
hono: "12KB",
express: "2.5MB with dependencies",
fastify: "780KB",
koa: "570KB"
}
// Edge compatibility
const edgeSupport = {
hono: "Native Workers, Deno, Bun support",
express: "Requires Node.js polyfills",
fastify: "Limited edge support",
koa: "No edge support"
}
Hono transforms Workers development by providing a modern, type-safe, and performant framework that feels familiar yet is optimized for the edge. Its minimal overhead, excellent TypeScript support, and comprehensive middleware ecosystem make it the perfect choice for building APIs on Cloudflare Workers.
Key advantages:
- Zero dependencies: 12KB total size
- Type safety: First-class TypeScript support
- Edge-first: Built for Workers' constraints
- Fast routing: Trie-based router outperforms regex
- Middleware ecosystem: Auth, validation, caching, and more
- Familiar API: Easy transition from Express/Koa
Hono provides the structure and tools needed for everything from simple APIs to complex applications, while maintaining the performance characteristics essential for edge computing.
Next: Additional Services - Queues, Durable Objects, and more
The Pitch: D1 gives you a real SQLite database with 5GB storage and 5M daily reads for free, running globally at the edge with zero cold starts.
The Win:
// Query users instantly from anywhere in the world const user = await env.DB .prepare('SELECT * FROM users WHERE email = ?') .bind('[email protected]') .first()The Catch: Eventually consistent replication means writes may take a few seconds to appear globally - design accordingly.
TL;DR - Key Takeaways
- What: SQLite-compatible database running at the edge
- Free Tier: 5GB storage, 5M rows read/day, 100K rows written/day
- Primary Use Cases: User data, content storage, session management, analytics
- Key Features: SQL compatibility, automatic replication, zero cold starts
- Limitations: Eventually consistent, 100MB/query result, no long transactions
D1 brings the power of SQLite to Cloudflare's global network, offering a real SQL database with ACID transactions, 5GB storage, and 5 million reads per day—all on the free tier. It's not a toy database; it's SQLite, the world's most deployed database, running at the edge.
Traditional cloud databases are expensive and regionally bound:
// Database cost comparison
const databaseComparison = {
awsRDS: {
minimumCost: "$15/month",
regions: "Single region (+100ms latency elsewhere)",
scaling: "Manual with downtime",
storage: "20GB minimum"
},
planetscale: {
freeTier: "10M row reads/month",
paidStarts: "$29/month",
regions: "Limited locations",
scaling: "Automatic but costly"
},
cloudflareD1: {
freeTier: "5GB storage, 5M reads/day",
performance: "SQLite at the edge",
scaling: "Automatic and global",
cost: "$0"
}
}
Create your first database:
# Create database
wrangler d1 create my-database
# Create schema
wrangler d1 execute my-database --file=./schema.sql
Define your schema:
-- schema.sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published, created_at);
interface Env {
DB: D1Database
}
export default {
async fetch(request: Request, env: Env) {
// Insert data
const user = await env.DB
.prepare('INSERT INTO users (email, name) VALUES (?, ?) RETURNING *')
.bind('[email protected]', 'John Doe')
.first()
// Query with prepared statements
const posts = await env.DB
.prepare(`
SELECT p.*, u.name as author_name
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.published = ?
ORDER BY p.created_at DESC
LIMIT ?
`)
.bind(true, 10)
.all()
return Response.json({
user,
posts: posts.results
})
}
}
D1 leverages SQLite's strengths while solving its traditional weaknesses:
Traditional SQLite:
- ✅ Fast, reliable, ACID compliant
- ❌ Single-writer limitation
- ❌ No network access
- ❌ Local file only
D1's Innovation:
- ✅ All SQLite benefits
- ✅ Distributed read replicas
- ✅ Network accessible
- ✅ Automatic replication
export async function transferFunds(env: Env, fromId: number, toId: number, amount: number) {
const tx = await env.DB.transaction(async (tx) => {
// Deduct from sender
await tx
.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?')
.bind(amount, fromId, amount)
.run()
// Add to receiver
await tx
.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
.bind(amount, toId)
.run()
// Log transaction
await tx
.prepare('INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)')
.bind(fromId, toId, amount)
.run()
})
return tx
}
export async function batchInsert(env: Env, users: User[]) {
const statements = users.map(user =>
env.DB
.prepare('INSERT INTO users (email, name) VALUES (?, ?)')
.bind(user.email, user.name)
)
const results = await env.DB.batch(statements)
return results
}
-- Create FTS5 table
CREATE VIRTUAL TABLE posts_fts USING fts5(
title,
content,
content=posts,
content_rowid=id
);
-- Populate FTS index
INSERT INTO posts_fts(posts_fts) VALUES('rebuild');
// Search implementation
export async function searchPosts(env: Env, query: string) {
const results = await env.DB
.prepare(`
SELECT
posts.*,
highlight(posts_fts, 0, '<mark>', '</mark>') as highlighted_title,
highlight(posts_fts, 1, '<mark>', '</mark>') as highlighted_content
FROM posts
JOIN posts_fts ON posts.id = posts_fts.rowid
WHERE posts_fts MATCH ?
ORDER BY rank
LIMIT 20
`)
.bind(query)
.all()
return results.results
}
// Tenant isolation pattern
export async function getTenantData(env: Env, tenantId: string, userId: string) {
// Always filter by tenant
const user = await env.DB
.prepare('SELECT * FROM users WHERE tenant_id = ? AND id = ?')
.bind(tenantId, userId)
.first()
const subscription = await env.DB
.prepare('SELECT * FROM subscriptions WHERE tenant_id = ?')
.bind(tenantId)
.first()
return { user, subscription }
}
// Row-level security
export async function createPost(env: Env, tenantId: string, userId: string, post: Post) {
return env.DB
.prepare(`
INSERT INTO posts (tenant_id, user_id, title, content)
VALUES (?, ?, ?, ?)
RETURNING *
`)
.bind(tenantId, userId, post.title, post.content)
.first()
}
// Event store schema
const eventSchema = `
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
event_data JSON NOT NULL,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_events_aggregate ON events(aggregate_id, created_at);
`
// Append events
export async function appendEvent(env: Env, event: Event) {
await env.DB
.prepare(`
INSERT INTO events (aggregate_id, event_type, event_data, metadata)
VALUES (?, ?, ?, ?)
`)
.bind(
event.aggregateId,
event.type,
JSON.stringify(event.data),
JSON.stringify(event.metadata)
)
.run()
}
// Replay events
export async function replayEvents(env: Env, aggregateId: string) {
const events = await env.DB
.prepare(`
SELECT * FROM events
WHERE aggregate_id = ?
ORDER BY created_at
`)
.bind(aggregateId)
.all()
return events.results.reduce((state, event) => {
return applyEvent(state, JSON.parse(event.event_data))
}, {})
}
// Optimized time-series schema
const timeSeriesSchema = `
CREATE TABLE metrics (
metric_name TEXT NOT NULL,
timestamp INTEGER NOT NULL,
value REAL NOT NULL,
tags JSON,
PRIMARY KEY (metric_name, timestamp)
) WITHOUT ROWID;
-- Partition index for efficient queries
CREATE INDEX idx_metrics_time ON metrics(timestamp);
`
// Efficient aggregation
export async function getMetricStats(env: Env, metric: string, hours: number) {
const since = Date.now() - (hours * 3600 * 1000)
return env.DB
.prepare(`
SELECT
strftime('%Y-%m-%d %H:00:00', datetime(timestamp/1000, 'unixepoch')) as hour,
AVG(value) as avg_value,
MIN(value) as min_value,
MAX(value) as max_value,
COUNT(*) as data_points
FROM metrics
WHERE metric_name = ? AND timestamp > ?
GROUP BY hour
ORDER BY hour DESC
`)
.bind(metric, since)
.all()
}
-- Covering index for common queries
CREATE INDEX idx_posts_listing ON posts(
published,
created_at DESC,
title,
user_id
) WHERE published = TRUE;
-- Partial index for active records
CREATE INDEX idx_users_active ON users(email)
WHERE deleted_at IS NULL;
-- Expression index for case-insensitive search
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
// Bad: N+1 queries
const posts = await env.DB.prepare('SELECT * FROM posts').all()
for (const post of posts.results) {
const author = await env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(post.user_id)
.first()
post.author = author
}
// Good: Single query with JOIN
const posts = await env.DB
.prepare(`
SELECT
p.*,
json_object(
'id', u.id,
'name', u.name,
'email', u.email
) as author
FROM posts p
JOIN users u ON p.user_id = u.id
`)
.all()
// D1 handles connection pooling automatically
// But you can optimize with caching
const cache = new Map()
export async function getCachedUser(env: Env, userId: string) {
const cacheKey = `user:${userId}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const user = await env.DB
.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first()
if (user) {
cache.set(cacheKey, user)
// Clear cache after 1 minute
setTimeout(() => cache.delete(cacheKey), 60000)
}
return user
}
// migrations/001_initial.sql
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Your schema here
// Worker migration runner
export async function runMigrations(env: Env) {
const migrations = [
{ version: 1, sql: await readFile('001_initial.sql') },
{ version: 2, sql: await readFile('002_add_posts.sql') },
]
for (const migration of migrations) {
const applied = await env.DB
.prepare('SELECT 1 FROM migrations WHERE version = ?')
.bind(migration.version)
.first()
if (!applied) {
await env.DB.transaction(async (tx) => {
await tx.exec(migration.sql)
await tx
.prepare('INSERT INTO migrations (version) VALUES (?)')
.bind(migration.version)
.run()
})
}
}
}
// Export data
export async function exportData(env: Env) {
const tables = ['users', 'posts', 'comments']
const backup = {}
for (const table of tables) {
const data = await env.DB
.prepare(`SELECT * FROM ${table}`)
.all()
backup[table] = data.results
}
// Store in R2
await env.BUCKET.put(
`backups/backup-${Date.now()}.json`,
JSON.stringify(backup)
)
}
// Point-in-time restore
export async function restore(env: Env, timestamp: number) {
const backup = await env.BUCKET.get(`backups/backup-${timestamp}.json`)
const data = await backup.json()
await env.DB.transaction(async (tx) => {
// Clear existing data
for (const table of Object.keys(data)) {
await tx.exec(`DELETE FROM ${table}`)
}
// Restore from backup
for (const [table, rows] of Object.entries(data)) {
for (const row of rows) {
const columns = Object.keys(row).join(', ')
const placeholders = Object.keys(row).map(() => '?').join(', ')
await tx
.prepare(`INSERT INTO ${table} (${columns}) VALUES (${placeholders})`)
.bind(...Object.values(row))
.run()
}
}
})
}
// Query performance tracking
export async function trackQueryPerformance(env: Env) {
const slowQueries = await env.DB
.prepare(`
SELECT
sql,
COUNT(*) as execution_count,
AVG(duration) as avg_duration,
MAX(duration) as max_duration
FROM query_log
WHERE duration > 100
GROUP BY sql
ORDER BY avg_duration DESC
LIMIT 10
`)
.all()
// Log to Analytics Engine
await env.ANALYTICS.writeDataPoint({
dataset: 'd1_performance',
point: {
slow_queries: slowQueries.results.length,
timestamp: Date.now()
}
})
}
const d1FreeTier = {
storage: "5GB total",
reads: "5 million/day",
writes: "100,000/day",
databases: "10 per account",
maxQueryTime: "30 seconds",
maxResultSize: "100MB",
}
const bestPractices = {
indexes: "Create indexes for all WHERE/JOIN columns",
transactions: "Keep transactions short",
batching: "Use batch() for multiple operations",
caching: "Cache frequently accessed data in KV",
archiving: "Move old data to R2"
}
// Performance comparison
const performance = {
d1: {
readLatency: "5-10ms at edge",
writeLatency: "20-50ms globally",
consistency: "Strong consistency"
},
postgres_regional: {
readLatency: "5-200ms (depends on distance)",
writeLatency: "10-300ms (depends on distance)",
consistency: "Strong consistency"
},
dynamodb: {
readLatency: "10-20ms in region",
writeLatency: "10-20ms in region",
consistency: "Eventual by default"
}
}
D1 brings production-grade SQL to the edge with zero cost. By leveraging SQLite's battle-tested reliability and Cloudflare's global network, it offers a unique combination of performance, features, and economics. The 5GB storage and 5 million daily reads support substantial applications—far beyond typical "free tier" limitations. It's the ideal solution for any application needing a relational database for structured data, from user accounts to complex application state.
Next: KV Store - Lightning-fast key-value storage at the edge
The Pitch: KV is a globally distributed key-value store that gives you 100K daily reads with 5-10ms latency worldwide - perfect for sessions, caching, and config.
The Win:
// Store and retrieve globally in milliseconds await env.KV.put('session:abc123', JSON.stringify(userData), { expirationTtl: 3600 // Auto-expires in 1 hour }) const session = await env.KV.get('session:abc123', 'json')The Catch: 1,000 writes/day limit on free tier - use it for read-heavy data like sessions and caching, not for primary data storage.
TL;DR - Key Takeaways
- What: Globally distributed key-value store with edge caching
- Free Tier: 100,000 reads/day, 1,000 writes/day, 1GB storage
- Primary Use Cases: Session storage, feature flags, caching, user preferences
- Key Features: Global replication, 60s consistency, unlimited key size
- Limitations: Eventually consistent, 1,000 writes/day, 25MB value size limit
Workers KV provides a globally distributed key-value store with eventual consistency, perfect for caching, session management, and configuration storage. With 100,000 reads and 1,000 writes daily on the free tier, plus 1GB of storage, it's an essential component of edge applications.
KV is designed for read-heavy workloads with global distribution:
// KV vs Traditional Solutions
const comparison = {
redis: {
latency: "1-5ms in region, 100ms+ elsewhere",
consistency: "Strong",
deployment: "Single region or complex clustering",
cost: "$15+/month minimum"
},
dynamodb: {
latency: "10-20ms in region",
consistency: "Configurable",
deployment: "Regional with global tables ($$$)",
cost: "$0.25/million reads"
},
workersKV: {
latency: "5-10ms globally",
consistency: "Eventual (60s globally)",
deployment: "Automatic global replication",
cost: "100K reads/day free"
}
}
Create a namespace:
# Create KV namespace
wrangler kv:namespace create "CACHE"
wrangler kv:namespace create "SESSIONS"
wrangler kv:namespace create "CONFIG"
Basic operations:
interface Env {
CACHE: KVNamespace
SESSIONS: KVNamespace
CONFIG: KVNamespace
}
export default {
async fetch(request: Request, env: Env) {
// Write data
await env.CACHE.put('user:123', JSON.stringify({
name: 'John Doe',
email: '[email protected]'
}), {
expirationTtl: 3600 // 1 hour
})
// Read data
const user = await env.CACHE.get('user:123', 'json')
// Delete data
await env.CACHE.delete('user:123')
// List keys
const list = await env.CACHE.list({ prefix: 'user:' })
return Response.json({ user, keys: list.keys })
}
}
interface Session {
userId: string
createdAt: number
data: Record<string, any>
}
export class SessionManager {
constructor(private kv: KVNamespace) {}
async create(userId: string): Promise<string> {
const sessionId = crypto.randomUUID()
const session: Session = {
userId,
createdAt: Date.now(),
data: {}
}
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(session),
{ expirationTtl: 86400 } // 24 hours
)
return sessionId
}
async get(sessionId: string): Promise<Session | null> {
return await this.kv.get(`session:${sessionId}`, 'json')
}
async update(sessionId: string, data: Record<string, any>) {
const session = await this.get(sessionId)
if (!session) throw new Error('Session not found')
session.data = { ...session.data, ...data }
await this.kv.put(
`session:${sessionId}`,
JSON.stringify(session),
{ expirationTtl: 86400 }
)
}
async destroy(sessionId: string) {
await this.kv.delete(`session:${sessionId}`)
}
}
// Usage in Worker
export default {
async fetch(request: Request, env: Env) {
const sessions = new SessionManager(env.SESSIONS)
// Create session on login
if (request.url.includes('/login')) {
const sessionId = await sessions.create('user123')
return new Response('Logged in', {
headers: {
'Set-Cookie': `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`
}
})
}
// Check session
const cookie = request.headers.get('Cookie')
const sessionId = cookie?.match(/session=([^;]+)/)?.[1]
if (sessionId) {
const session = await sessions.get(sessionId)
if (session) {
// User is authenticated
return Response.json({ user: session.userId })
}
}
return new Response('Not authenticated', { status: 401 })
}
}
export class CacheManager {
constructor(
private kv: KVNamespace,
private options: {
ttl?: number
staleWhileRevalidate?: number
} = {}
) {}
async get<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
// Try cache first
const cached = await this.kv.get(key, 'json') as {
data: T
expires: number
stale: number
} | null
const now = Date.now()
// Return fresh cache
if (cached && cached.expires > now) {
return cached.data
}
// Return stale while revalidating
if (cached && cached.stale > now) {
// Revalidate in background
this.revalidate(key, fetcher, ttl)
return cached.data
}
// Cache miss - fetch fresh data
const data = await fetcher()
await this.set(key, data, ttl)
return data
}
private async set<T>(key: string, data: T, ttl?: number) {
const cacheTtl = ttl || this.options.ttl || 3600
const staleTtl = this.options.staleWhileRevalidate || 86400
await this.kv.put(key, JSON.stringify({
data,
expires: Date.now() + (cacheTtl * 1000),
stale: Date.now() + (staleTtl * 1000)
}), {
expirationTtl: staleTtl
})
}
private async revalidate<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
) {
try {
const data = await fetcher()
await this.set(key, data, ttl)
} catch (error) {
console.error('Revalidation failed:', error)
}
}
}
// Usage
export default {
async fetch(request: Request, env: Env) {
const cache = new CacheManager(env.CACHE, {
ttl: 300, // 5 minutes
staleWhileRevalidate: 3600 // 1 hour
})
// Expensive API call cached
const data = await cache.get(
'api:users',
async () => {
const response = await fetch('https://api.example.com/users')
return response.json()
}
)
return Response.json(data)
}
}
interface FeatureFlag {
enabled: boolean
rolloutPercentage?: number
allowedUsers?: string[]
metadata?: Record<string, any>
}
export class FeatureFlagManager {
constructor(private kv: KVNamespace) {}
async isEnabled(
flagName: string,
userId?: string
): Promise<boolean> {
const flag = await this.kv.get<FeatureFlag>(
`flag:${flagName}`,
'json'
)
if (!flag) return false
if (!flag.enabled) return false
// Check allowed users
if (flag.allowedUsers?.includes(userId || '')) {
return true
}
// Check rollout percentage
if (flag.rolloutPercentage) {
const hash = await this.hash(flagName + userId)
const threshold = flag.rolloutPercentage / 100
return hash < threshold
}
return flag.enabled
}
private async hash(input: string): Promise<number> {
const encoder = new TextEncoder()
const data = encoder.encode(input)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = new Uint8Array(hashBuffer)
// Convert to number between 0 and 1
return hashArray[0] / 255
}
async setFlag(flagName: string, flag: FeatureFlag) {
await this.kv.put(
`flag:${flagName}`,
JSON.stringify(flag)
)
}
}
// Usage
export default {
async fetch(request: Request, env: Env) {
const flags = new FeatureFlagManager(env.CONFIG)
const userId = request.headers.get('X-User-ID')
const features = {
newUI: await flags.isEnabled('new-ui', userId),
betaFeature: await flags.isEnabled('beta-feature', userId),
experiments: await flags.isEnabled('experiments', userId)
}
return Response.json({ features })
}
}
export class RateLimiter {
constructor(
private kv: KVNamespace,
private config: {
windowMs: number
maxRequests: number
}
) {}
async check(identifier: string): Promise<{
allowed: boolean
remaining: number
resetAt: number
}> {
const key = `rate:${identifier}`
const now = Date.now()
const windowStart = now - this.config.windowMs
// Get current window data
const data = await this.kv.get<{
requests: Array<number>
}>(key, 'json') || { requests: [] }
// Filter out old requests
data.requests = data.requests.filter(time => time > windowStart)
const allowed = data.requests.length < this.config.maxRequests
if (allowed) {
data.requests.push(now)
await this.kv.put(key, JSON.stringify(data), {
expirationTtl: Math.ceil(this.config.windowMs / 1000)
})
}
return {
allowed,
remaining: Math.max(0, this.config.maxRequests - data.requests.length),
resetAt: windowStart + this.config.windowMs
}
}
}
// Usage
export default {
async fetch(request: Request, env: Env) {
const limiter = new RateLimiter(env.CACHE, {
windowMs: 60000, // 1 minute
maxRequests: 100
})
const ip = request.headers.get('CF-Connecting-IP') || 'unknown'
const { allowed, remaining, resetAt } = await limiter.check(ip)
if (!allowed) {
return new Response('Rate limit exceeded', {
status: 429,
headers: {
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': new Date(resetAt).toISOString()
}
})
}
// Process request
return new Response('OK', {
headers: {
'X-RateLimit-Remaining': remaining.toString()
}
})
}
}
interface Config {
apiKeys: Record<string, string>
features: Record<string, boolean>
limits: Record<string, number>
urls: Record<string, string>
}
export class ConfigManager {
private cache: Config | null = null
private lastFetch = 0
private ttl = 60000 // 1 minute
constructor(private kv: KVNamespace) {}
async get(): Promise<Config> {
const now = Date.now()
// Return cached if fresh
if (this.cache && (now - this.lastFetch) < this.ttl) {
return this.cache
}
// Fetch from KV
const config = await this.kv.get<Config>('config:global', 'json')
if (!config) {
throw new Error('Configuration not found')
}
this.cache = config
this.lastFetch = now
return config
}
async update(updates: Partial<Config>) {
const current = await this.get()
const updated = { ...current, ...updates }
await this.kv.put('config:global', JSON.stringify(updated))
// Invalidate cache
this.cache = null
}
}
// Usage
export default {
async fetch(request: Request, env: Env) {
const config = new ConfigManager(env.CONFIG)
// Get configuration
const { apiKeys, features, limits } = await config.get()
// Check API key
const apiKey = request.headers.get('X-API-Key')
if (!apiKey || !Object.values(apiKeys).includes(apiKey)) {
return new Response('Invalid API key', { status: 401 })
}
// Check feature flag
if (!features.betaApi) {
return new Response('Beta API is disabled', { status: 503 })
}
return Response.json({
message: 'Welcome to the API',
rateLimit: limits.apiCalls
})
}
}
export class DistributedLock {
constructor(private kv: KVNamespace) {}
async acquire(
resource: string,
ttl: number = 30
): Promise<string | null> {
const lockId = crypto.randomUUID()
const key = `lock:${resource}`
// Try to acquire lock
const success = await this.kv.put(key, lockId, {
expirationTtl: ttl,
// Only set if key doesn't exist
expiration: Date.now() + (ttl * 1000)
})
// Verify we got the lock
const current = await this.kv.get(key)
return current === lockId ? lockId : null
}
async release(resource: string, lockId: string) {
const key = `lock:${resource}`
const current = await this.kv.get(key)
// Only release if we own the lock
if (current === lockId) {
await this.kv.delete(key)
}
}
async withLock<T>(
resource: string,
fn: () => Promise<T>
): Promise<T> {
const lockId = await this.acquire(resource)
if (!lockId) {
throw new Error('Failed to acquire lock')
}
try {
return await fn()
} finally {
await this.release(resource, lockId)
}
}
}
interface Event {
id: string
type: string
aggregateId: string
data: any
timestamp: number
}
export class EventStore {
constructor(private kv: KVNamespace) {}
async append(event: Event) {
const key = `events:${event.aggregateId}:${event.timestamp}:${event.id}`
await this.kv.put(key, JSON.stringify(event), {
metadata: {
type: event.type,
aggregateId: event.aggregateId
}
})
}
async getEvents(
aggregateId: string,
fromTimestamp?: number
): Promise<Event[]> {
const prefix = `events:${aggregateId}:`
const start = fromTimestamp
? `${prefix}${fromTimestamp}`
: prefix
const list = await this.kv.list({
prefix,
start
})
const events = await Promise.all(
list.keys.map(async (key) => {
const event = await this.kv.get(key.name, 'json')
return event as Event
})
)
return events.filter(Boolean)
}
async getLatestSnapshot(aggregateId: string) {
return await this.kv.get(
`snapshot:${aggregateId}`,
'json'
)
}
async saveSnapshot(aggregateId: string, state: any) {
await this.kv.put(
`snapshot:${aggregateId}`,
JSON.stringify({
state,
timestamp: Date.now()
})
)
}
}
export async function batchGet(
kv: KVNamespace,
keys: string[]
): Promise<Map<string, any>> {
const results = new Map()
// KV doesn't have native batch, so parallelize
const promises = keys.map(async (key) => {
const value = await kv.get(key, 'json')
if (value !== null) {
results.set(key, value)
}
})
await Promise.all(promises)
return results
}
// Usage with caching
export class BatchCache {
private pending = new Map<string, Promise<any>>()
constructor(private kv: KVNamespace) {}
async get(key: string): Promise<any> {
// Check if already fetching
if (this.pending.has(key)) {
return this.pending.get(key)
}
// Create promise
const promise = this.kv.get(key, 'json')
this.pending.set(key, promise)
// Clean up after resolution
promise.finally(() => this.pending.delete(key))
return promise
}
}
export class CacheWarmer {
constructor(
private kv: KVNamespace,
private db: D1Database
) {}
async warmCache(keys: string[]) {
const missingKeys: string[] = []
// Check what's missing
for (const key of keys) {
const exists = await this.kv.get(key)
if (!exists) {
missingKeys.push(key)
}
}
// Batch fetch from database
if (missingKeys.length > 0) {
const data = await this.fetchFromDatabase(missingKeys)
// Store in KV
await Promise.all(
data.map(item =>
this.kv.put(
item.key,
JSON.stringify(item.value),
{ expirationTtl: 3600 }
)
)
)
}
}
private async fetchFromDatabase(keys: string[]) {
// Implementation depends on your schema
return []
}
}
const kvLimits = {
keySize: "512 bytes",
valueSize: "25 MB",
metadataSize: "1024 bytes",
listLimit: "1000 keys per operation",
ttlMax: "365 days",
consistency: "Eventual (60 seconds globally)"
}
const bestPractices = {
keys: "Use prefixes for organization (user:123, session:abc)",
values: "JSON for structured data, text for simple values",
ttl: "Always set expiration for temporary data",
consistency: "Don't rely on immediate global consistency",
costs: "Optimize reads over writes (100:1 ratio in free tier)"
}
// When to use each Cloudflare storage option
const storageGuide = {
kv: {
useFor: ["Sessions", "Cache", "Config", "Feature flags"],
strengths: "Global distribution, fast reads",
weaknesses: "Eventual consistency, limited queries"
},
d1: {
useFor: ["Relational data", "Transactions", "Complex queries"],
strengths: "SQL, ACID, strong consistency",
weaknesses: "Regional writes, query limits"
},
r2: {
useFor: ["Files", "Images", "Backups", "Large data"],
strengths: "S3 compatible, no egress fees",
weaknesses: "Not for small, frequent updates"
},
durableObjects: {
useFor: ["Real-time", "Coordination", "Stateful logic"],
strengths: "Strong consistency, WebSockets",
weaknesses: "Single location, higher cost"
}
}
Workers KV provides the perfect solution for global state management at the edge. With its generous free tier (100,000 reads daily), global distribution, and simple API, it excels at caching, session management, and configuration storage. While eventual consistency means it's not suitable for every use case, KV's performance characteristics make it ideal for read-heavy workloads that benefit from global distribution.
Next: R2 Storage - Object storage with zero egress fees
The Pitch: R2 is S3-compatible object storage with the game-changing difference: zero egress fees. Store 10GB and serve unlimited downloads for free.
The Win:
// Upload once, serve millions of times for $0 await env.BUCKET.put('images/logo.png', imageBuffer) const image = await env.BUCKET.get('images/logo.png') return new Response(image.body, { headers: { 'Content-Type': 'image/png' } })The Catch: No public bucket access on free tier - you must serve files through Workers, which counts against your 100K daily requests.
TL;DR - Key Takeaways
- What: S3-compatible object storage with zero egress fees
- Free Tier: 10GB storage, 1M Class A ops, 10M Class B ops/month
- Primary Use Cases: File uploads, media storage, backups, static assets
- Key Features: S3 API compatibility, zero egress fees, automatic replication
- Limitations: No public buckets on free tier, 100MB upload limit per request
R2's killer feature: zero egress fees. While S3 charges $0.09/GB for data transfer, R2 charges nothing. This single difference can reduce storage costs by 95% for content-heavy applications.
// The hidden cost of cloud storage
const monthlyStorageCost = {
// Storing 1TB of images
s3: {
storage: 1000 * 0.023, // $23/month
egress: 5000 * 0.09, // $450/month (5TB transfer)
total: 473 // $473/month
},
r2: {
storage: 1000 * 0.015, // $15/month
egress: 0, // $0 (unlimited)
total: 15 // $15/month
}
}
// R2 is 31x cheaper for this use case!
Create a bucket:
# Create R2 bucket
wrangler r2 bucket create my-bucket
# Upload files
wrangler r2 object put my-bucket/hello.txt --file ./hello.txt
Basic operations in Workers:
interface Env {
BUCKET: R2Bucket
}
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url)
const key = url.pathname.slice(1)
switch (request.method) {
case 'GET':
const object = await env.BUCKET.get(key)
if (!object) {
return new Response('Object Not Found', { status: 404 })
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)
return new Response(object.body, { headers })
case 'PUT':
await env.BUCKET.put(key, request.body, {
httpMetadata: request.headers
})
return new Response('Created', { status: 201 })
case 'DELETE':
await env.BUCKET.delete(key)
return new Response('Deleted', { status: 204 })
default:
return new Response('Method Not Allowed', { status: 405 })
}
}
}
interface ImageUploadEnv extends Env {
IMAGES: R2Bucket
THUMBNAILS: R2Bucket
}
export class ImageService {
constructor(private env: ImageUploadEnv) {}
async upload(request: Request): Promise<Response> {
const formData = await request.formData()
const file = formData.get('image') as File
if (!file) {
return new Response('No image provided', { status: 400 })
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
return new Response('Invalid file type', { status: 400 })
}
// Generate unique filename
const ext = file.name.split('.').pop()
const filename = `${crypto.randomUUID()}.${ext}`
const key = `uploads/${new Date().toISOString().split('T')[0]}/${filename}`
// Upload original
await this.env.IMAGES.put(key, file, {
httpMetadata: {
contentType: file.type,
},
customMetadata: {
originalName: file.name,
uploadedAt: new Date().toISOString(),
size: file.size.toString()
}
})
// Generate thumbnail in background
const ctx = this.env.ctx as ExecutionContext
ctx.waitUntil(this.generateThumbnail(key, file))
return Response.json({
key,
url: `/images/${key}`,
size: file.size,
type: file.type
})
}
private async generateThumbnail(key: string, file: File) {
// Use Cloudflare Image Resizing API
const response = await fetch(`/cdn-cgi/image/width=200/${key}`)
const thumbnail = await response.blob()
const thumbnailKey = key.replace('/uploads/', '/thumbnails/')
await this.env.THUMBNAILS.put(thumbnailKey, thumbnail, {
httpMetadata: {
contentType: file.type,
}
})
}
async serve(key: string): Promise<Response> {
const object = await this.env.IMAGES.get(key)
if (!object) {
return new Response('Not Found', { status: 404 })
}
const headers = new Headers()
object.writeHttpMetadata(headers)
// Add caching headers
headers.set('Cache-Control', 'public, max-age=31536000')
headers.set('ETag', object.httpEtag)
// Check if-none-match
const ifNoneMatch = this.env.request.headers.get('If-None-Match')
if (ifNoneMatch === object.httpEtag) {
return new Response(null, { status: 304, headers })
}
return new Response(object.body, { headers })
}
}
// Worker implementation
export default {
async fetch(request: Request, env: ImageUploadEnv) {
const url = new URL(request.url)
const imageService = new ImageService(env)
if (url.pathname === '/upload' && request.method === 'POST') {
return imageService.upload(request)
}
if (url.pathname.startsWith('/images/')) {
const key = url.pathname.slice('/images/'.length)
return imageService.serve(key)
}
return new Response('Not Found', { status: 404 })
}
}
export class SecureFileShare {
constructor(
private bucket: R2Bucket,
private kv: KVNamespace
) {}
async createShareLink(
key: string,
options: {
expiresIn?: number
maxDownloads?: number
password?: string
} = {}
): Promise<string> {
const shareId = crypto.randomUUID()
const shareData = {
key,
createdAt: Date.now(),
expiresAt: Date.now() + (options.expiresIn || 86400000), // 24h default
maxDownloads: options.maxDownloads || null,
downloads: 0,
password: options.password
? await this.hashPassword(options.password)
: null
}
await this.kv.put(
`share:${shareId}`,
JSON.stringify(shareData),
{
expirationTtl: Math.ceil((shareData.expiresAt - Date.now()) / 1000)
}
)
return shareId
}
async accessShare(
shareId: string,
password?: string
): Promise<Response> {
const shareData = await this.kv.get(`share:${shareId}`, 'json') as any
if (!shareData) {
return new Response('Share link not found', { status: 404 })
}
// Check expiration
if (Date.now() > shareData.expiresAt) {
await this.kv.delete(`share:${shareId}`)
return new Response('Share link expired', { status: 410 })
}
// Check password
if (shareData.password && !await this.verifyPassword(password || '', shareData.password)) {
return new Response('Invalid password', { status: 401 })
}
// Check download limit
if (shareData.maxDownloads && shareData.downloads >= shareData.maxDownloads) {
return new Response('Download limit exceeded', { status: 410 })
}
// Get file from R2
const object = await this.bucket.get(shareData.key)
if (!object) {
return new Response('File not found', { status: 404 })
}
// Update download count
shareData.downloads++
if (shareData.maxDownloads && shareData.downloads >= shareData.maxDownloads) {
await this.kv.delete(`share:${shareId}`)
} else {
await this.kv.put(`share:${shareId}`, JSON.stringify(shareData))
}
// Return file
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('Content-Disposition', `attachment; filename="${shareData.key.split('/').pop()}"`)
return new Response(object.body, { headers })
}
private async hashPassword(password: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(password)
const hash = await crypto.subtle.digest('SHA-256', data)
return btoa(String.fromCharCode(...new Uint8Array(hash)))
}
private async verifyPassword(password: string, hash: string): Promise<boolean> {
const passwordHash = await this.hashPassword(password)
return passwordHash === hash
}
}
interface CMSFile {
key: string
name: string
type: string
size: number
uploadedAt: string
metadata: Record<string, string>
}
export class ContentManagementSystem {
constructor(
private bucket: R2Bucket,
private db: D1Database
) {}
async uploadFile(
file: File,
folder: string,
metadata: Record<string, string> = {}
): Promise<CMSFile> {
const key = `${folder}/${Date.now()}-${file.name}`
// Upload to R2
await this.bucket.put(key, file, {
httpMetadata: {
contentType: file.type,
cacheControl: 'public, max-age=31536000'
},
customMetadata: metadata
})
// Store metadata in D1
const cmsFile: CMSFile = {
key,
name: file.name,
type: file.type,
size: file.size,
uploadedAt: new Date().toISOString(),
metadata
}
await this.db.prepare(`
INSERT INTO files (key, name, type, size, folder, metadata, uploaded_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).bind(
key,
file.name,
file.type,
file.size,
folder,
JSON.stringify(metadata),
cmsFile.uploadedAt
).run()
return cmsFile
}
async listFiles(
folder: string,
options: {
limit?: number
cursor?: string
search?: string
} = {}
): Promise<{
files: CMSFile[]
cursor?: string
}> {
let query = `
SELECT * FROM files
WHERE folder = ?
`
const params: any[] = [folder]
if (options.search) {
query += ` AND name LIKE ?`
params.push(`%${options.search}%`)
}
query += ` ORDER BY uploaded_at DESC LIMIT ?`
params.push(options.limit || 50)
if (options.cursor) {
query += ` OFFSET ?`
params.push(parseInt(options.cursor))
}
const result = await this.db.prepare(query)
.bind(...params)
.all()
const files = result.results.map(row => ({
key: row.key,
name: row.name,
type: row.type,
size: row.size,
uploadedAt: row.uploaded_at,
metadata: JSON.parse(row.metadata)
}))
const nextCursor = files.length === (options.limit || 50)
? ((parseInt(options.cursor || '0')) + files.length).toString()
: undefined
return { files, cursor: nextCursor }
}
async deleteFile(key: string): Promise<void> {
// Delete from R2
await this.bucket.delete(key)
// Delete from database
await this.db.prepare('DELETE FROM files WHERE key = ?')
.bind(key)
.run()
}
async generateSignedUrl(
key: string,
expiresIn: number = 3600
): Promise<string> {
// For now, return a Worker URL
// In production, use R2 presigned URLs when available
return `/api/files/${encodeURIComponent(key)}`
}
}
export class BackupService {
constructor(
private bucket: R2Bucket,
private db: D1Database
) {}
async createBackup(name: string): Promise<string> {
const timestamp = new Date().toISOString()
const backupKey = `backups/${name}-${timestamp}.tar.gz`
// Export database
const tables = ['users', 'posts', 'comments', 'files']
const dbExport: Record<string, any[]> = {}
for (const table of tables) {
const data = await this.db.prepare(`SELECT * FROM ${table}`).all()
dbExport[table] = data.results
}
// Create tar.gz archive
const archive = await this.createArchive({
'database.json': JSON.stringify(dbExport, null, 2),
'metadata.json': JSON.stringify({
name,
timestamp,
version: '1.0',
tables: tables.length,
totalRecords: Object.values(dbExport).reduce((sum, t) => sum + t.length, 0)
}, null, 2)
})
// Upload to R2
await this.bucket.put(backupKey, archive, {
httpMetadata: {
contentType: 'application/gzip',
contentEncoding: 'gzip'
},
customMetadata: {
name,
timestamp,
type: 'full-backup'
}
})
// Record backup in database
await this.db.prepare(`
INSERT INTO backups (key, name, size, created_at)
VALUES (?, ?, ?, ?)
`).bind(backupKey, name, archive.size, timestamp).run()
return backupKey
}
async restoreBackup(backupKey: string): Promise<void> {
// Get backup from R2
const backup = await this.bucket.get(backupKey)
if (!backup) {
throw new Error('Backup not found')
}
// Extract archive
const archive = await this.extractArchive(await backup.arrayBuffer())
const dbExport = JSON.parse(archive['database.json'])
// Restore database
await this.db.transaction(async (tx) => {
// Clear existing data
for (const table of Object.keys(dbExport)) {
await tx.prepare(`DELETE FROM ${table}`).run()
}
// Import data
for (const [table, rows] of Object.entries(dbExport)) {
for (const row of rows as any[]) {
const columns = Object.keys(row)
const placeholders = columns.map(() => '?').join(', ')
await tx.prepare(`
INSERT INTO ${table} (${columns.join(', ')})
VALUES (${placeholders})
`).bind(...Object.values(row)).run()
}
}
})
}
async listBackups(): Promise<Array<{
key: string
name: string
size: number
createdAt: string
}>> {
const result = await this.db.prepare(`
SELECT * FROM backups
ORDER BY created_at DESC
LIMIT 100
`).all()
return result.results as any[]
}
private async createArchive(files: Record<string, string>): Promise<Blob> {
// Simplified - in production use a proper tar library
const boundary = '----CloudflareArchiveBoundary'
const parts: string[] = []
for (const [filename, content] of Object.entries(files)) {
parts.push(`--${boundary}`)
parts.push(`Content-Disposition: form-data; name="file"; filename="${filename}"`)
parts.push('')
parts.push(content)
}
parts.push(`--${boundary}--`)
return new Blob([parts.join('\r\n')], { type: 'multipart/form-data' })
}
private async extractArchive(data: ArrayBuffer): Promise<Record<string, string>> {
// Simplified - in production use a proper tar library
const text = new TextDecoder().decode(data)
const files: Record<string, string> = {}
// Parse multipart data
const parts = text.split('----CloudflareArchiveBoundary')
for (const part of parts) {
const match = part.match(/filename="([^"]+)"[\r\n]+(.+)/s)
if (match) {
files[match[1]] = match[2].trim()
}
}
return files
}
}
export class LargeFileUploader {
constructor(private bucket: R2Bucket) {}
async uploadLargeFile(
key: string,
file: ReadableStream,
size: number
): Promise<void> {
const partSize = 10 * 1024 * 1024 // 10MB chunks
const numParts = Math.ceil(size / partSize)
// Initiate multipart upload
const upload = await this.bucket.createMultipartUpload(key)
try {
const parts: R2UploadedPart[] = []
const reader = file.getReader()
for (let partNumber = 1; partNumber <= numParts; partNumber++) {
const chunk = await this.readChunk(reader, partSize)
const part = await upload.uploadPart(partNumber, chunk)
parts.push(part)
}
// Complete upload
await upload.complete(parts)
} catch (error) {
// Abort on error
await upload.abort()
throw error
}
}
private async readChunk(
reader: ReadableStreamDefaultReader,
size: number
): Promise<ArrayBuffer> {
const chunks: Uint8Array[] = []
let totalSize = 0
while (totalSize < size) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
totalSize += value.length
}
// Combine chunks
const result = new Uint8Array(totalSize)
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
return result.buffer
}
}
export class LifecycleManager {
constructor(
private bucket: R2Bucket,
private kv: KVNamespace
) {}
async applyLifecycleRules(): Promise<void> {
// List old temporary files
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
const list = await this.bucket.list({
prefix: 'temp/',
limit: 1000
})
for (const object of list.objects) {
if (object.uploaded.getTime() < thirtyDaysAgo) {
await this.bucket.delete(object.key)
}
}
// Archive old backups
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000)
const backups = await this.bucket.list({
prefix: 'backups/',
limit: 1000
})
for (const backup of backups.objects) {
if (backup.uploaded.getTime() < ninetyDaysAgo) {
// Move to cold storage (simulated)
const object = await this.bucket.get(backup.key)
if (object) {
await this.bucket.put(
backup.key.replace('backups/', 'archive/'),
object.body,
{
customMetadata: {
archivedAt: new Date().toISOString(),
originalKey: backup.key
}
}
)
await this.bucket.delete(backup.key)
}
}
}
}
}
export class CachedStorage {
constructor(
private bucket: R2Bucket,
private cache: KVNamespace
) {}
async get(key: string): Promise<Response> {
// Check KV cache first
const cached = await this.cache.get(key, 'stream')
if (cached) {
return new Response(cached, {
headers: {
'X-Cache': 'HIT',
'Cache-Control': 'public, max-age=3600'
}
})
}
// Fetch from R2
const object = await this.bucket.get(key)
if (!object) {
return new Response('Not Found', { status: 404 })
}
// Cache small files in KV
if (object.size < 1024 * 1024) { // 1MB
const arrayBuffer = await object.arrayBuffer()
await this.cache.put(key, arrayBuffer, {
expirationTtl: 3600
})
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('X-Cache', 'MISS')
return new Response(object.body, { headers })
}
}
export async function handleConditionalRequest(
request: Request,
bucket: R2Bucket,
key: string
): Promise<Response> {
const ifNoneMatch = request.headers.get('If-None-Match')
const ifModifiedSince = request.headers.get('If-Modified-Since')
const object = await bucket.get(key, {
onlyIf: {
etagDoesNotMatch: ifNoneMatch || undefined,
uploadedAfter: ifModifiedSince
? new Date(ifModifiedSince)
: undefined
}
})
if (!object) {
return new Response(null, {
status: 304,
headers: {
'Cache-Control': 'public, max-age=31536000'
}
})
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('ETag', object.httpEtag)
headers.set('Last-Modified', object.uploaded.toUTCString())
return new Response(object.body, { headers })
}
export class StorageTiering {
constructor(
private hotBucket: R2Bucket, // Frequently accessed
private coldBucket: R2Bucket, // Archival
private db: D1Database
) {}
async moveToArchive(key: string): Promise<void> {
// Get from hot storage
const object = await this.hotBucket.get(key)
if (!object) return
// Copy to cold storage
await this.coldBucket.put(key, object.body, {
httpMetadata: object.httpMetadata,
customMetadata: {
...object.customMetadata,
archivedAt: new Date().toISOString(),
originalBucket: 'hot'
}
})
// Update database
await this.db.prepare(`
UPDATE files
SET storage_tier = 'cold', archived_at = ?
WHERE key = ?
`).bind(new Date().toISOString(), key).run()
// Delete from hot storage
await this.hotBucket.delete(key)
}
async retrieveFromArchive(key: string): Promise<void> {
// Get from cold storage
const object = await this.coldBucket.get(key)
if (!object) return
// Copy back to hot storage
await this.hotBucket.put(key, object.body, {
httpMetadata: object.httpMetadata,
customMetadata: object.customMetadata
})
// Update database
await this.db.prepare(`
UPDATE files
SET storage_tier = 'hot', archived_at = NULL
WHERE key = ?
`).bind(key).run()
}
}
const r2Limits = {
storage: "10GB free",
operations: {
classA: "1 million/month free", // PUT, POST, LIST
classB: "10 million/month free" // GET, HEAD
},
objectSize: "5TB maximum",
partSize: "5GB maximum",
egress: "Unlimited free",
buckets: "1000 per account"
}
const bestPractices = {
naming: "Use prefixes for organization (images/, backups/)",
caching: "Set appropriate Cache-Control headers",
compression: "Compress text files before storage",
metadata: "Use custom metadata for searchability",
lifecycle: "Implement cleanup for temporary files"
}
// Real-world cost comparison (1TB storage, 10TB egress/month)
const monthlyComparison = {
s3: {
storage: 23,
egress: 900, // $0.09/GB
requests: 5,
total: 928
},
cloudfront: {
storage: 23,
egress: 850, // $0.085/GB
requests: 5,
total: 878
},
r2: {
storage: 15,
egress: 0, // Free!
requests: 0, // 10M free
total: 15
}
}
// R2 is 62x cheaper for this use case
R2 fundamentally changes the economics of object storage by eliminating egress fees. This single innovation enables use cases that were previously cost-prohibitive: media streaming, software distribution, backup services, and content-heavy applications. The generous free tier (10GB storage, unlimited egress) makes R2 perfect for any application that stores and serves files, from user uploads to static assets.
Next: Vectorize - AI-powered vector search at the edge
The Pitch: Vectorize enables semantic search that understands meaning - find "jogging sneakers" when users search for "running shoes" across 5M vectors for free.
The Win:
// Find semantically similar content, not just keyword matches const embedding = await env.AI.run('@cf/baai/bge-base-en-v1.5', { text: ["best laptop for coding"] }) const results = await env.VECTORIZE.query(embedding.data[0], { topK: 5 })The Catch: 30,000 queries/month limit means ~1,000 searches/day - perfect for features and demos, but you'll need optimization for high-traffic search.
TL;DR - Key Takeaways
- What: Vector database for semantic search and AI applications
- Free Tier: 5M vectors, 30,000 queries/month, 5 indexes
- Primary Use Cases: Semantic search, recommendations, RAG, similarity matching
- Key Features: Multiple distance metrics, metadata filtering, global distribution
- Limitations: 1536 dimensions max, 10KB metadata per vector
Vector search understands meaning, not just keywords. While traditional search would miss "jogging sneakers" when searching for "running shoes," Vectorize finds semantically similar content across 5 million vectors on the free tier.
// Traditional search limitations
const keywordSearch = {
query: "running shoes",
matches: ["running shoes", "shoes for running"],
misses: ["jogging sneakers", "marathon footwear", "athletic trainers"]
}
// Vector search understanding
const vectorSearch = {
query: "running shoes",
matches: [
"jogging sneakers", // Semantically similar
"marathon footwear", // Contextually related
"athletic trainers", // Conceptually connected
"track and field shoes" // Domain relevant
]
}
Create an index:
# Create vector index
wrangler vectorize create my-index \
--dimensions=768 \
--metric=cosine
Basic operations:
interface Env {
VECTORIZE: VectorizeIndex
AI: Ai
}
export default {
async fetch(request: Request, env: Env) {
// Generate embedding
const text = "What is the best laptop for programming?"
const embedResponse = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
text: [text]
})
const embedding = embedResponse.data[0]
// Store vector with metadata
await env.VECTORIZE.insert([
{
id: 'doc-1',
values: embedding,
metadata: {
title: 'Best Programming Laptops 2024',
category: 'technology',
url: '/blog/best-laptops-2024'
}
}
])
// Search similar vectors
const results = await env.VECTORIZE.query(embedding, {
topK: 5,
filter: { category: 'technology' }
})
return Response.json({
query: text,
results: results.matches.map(match => ({
score: match.score,
...match.metadata
}))
})
}
}
export class DocumentSearch {
constructor(
private vectorize: VectorizeIndex,
private ai: Ai,
private db: D1Database
) {}
async indexDocument(doc: {
id: string
title: string
content: string
metadata?: Record<string, any>
}): Promise<void> {
// Split into chunks for better search
const chunks = this.chunkText(doc.content, 500)
const vectors: VectorizeVector[] = []
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i]
// Generate embedding
const response = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [chunk.text]
})
vectors.push({
id: `${doc.id}-chunk-${i}`,
values: response.data[0],
metadata: {
docId: doc.id,
title: doc.title,
chunkIndex: i,
text: chunk.text,
startOffset: chunk.start,
endOffset: chunk.end,
...doc.metadata
}
})
}
// Store vectors
await this.vectorize.insert(vectors)
// Store document in database
await this.db.prepare(`
INSERT INTO documents (id, title, content, metadata, indexed_at)
VALUES (?, ?, ?, ?, ?)
`).bind(
doc.id,
doc.title,
doc.content,
JSON.stringify(doc.metadata || {}),
new Date().toISOString()
).run()
}
async search(
query: string,
options: {
limit?: number
filter?: Record<string, any>
includeContent?: boolean
} = {}
): Promise<Array<{
docId: string
title: string
score: number
excerpt: string
metadata: any
}>> {
// Generate query embedding
const response = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [query]
})
const queryEmbedding = response.data[0]
// Search vectors
const results = await this.vectorize.query(queryEmbedding, {
topK: options.limit || 10,
filter: options.filter
})
// Group by document and get best match per doc
const docScores = new Map<string, any>()
for (const match of results.matches) {
const docId = match.metadata.docId
if (!docScores.has(docId) || match.score > docScores.get(docId).score) {
docScores.set(docId, {
docId,
title: match.metadata.title,
score: match.score,
excerpt: this.generateExcerpt(match.metadata.text, query),
metadata: match.metadata
})
}
}
// Sort by score and return
return Array.from(docScores.values())
.sort((a, b) => b.score - a.score)
.slice(0, options.limit || 10)
}
private chunkText(
text: string,
chunkSize: number
): Array<{ text: string; start: number; end: number }> {
const chunks: Array<{ text: string; start: number; end: number }> = []
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]
let currentChunk = ''
let currentStart = 0
let currentPos = 0
for (const sentence of sentences) {
if (currentChunk.length + sentence.length > chunkSize && currentChunk) {
chunks.push({
text: currentChunk.trim(),
start: currentStart,
end: currentPos
})
currentChunk = sentence
currentStart = currentPos
} else {
currentChunk += ' ' + sentence
}
currentPos += sentence.length
}
if (currentChunk) {
chunks.push({
text: currentChunk.trim(),
start: currentStart,
end: currentPos
})
}
return chunks
}
private generateExcerpt(text: string, query: string): string {
const queryWords = query.toLowerCase().split(/\s+/)
const sentences = text.split(/[.!?]+/)
// Find sentence with most query words
let bestSentence = sentences[0]
let maxMatches = 0
for (const sentence of sentences) {
const sentenceLower = sentence.toLowerCase()
const matches = queryWords.filter(word => sentenceLower.includes(word)).length
if (matches > maxMatches) {
maxMatches = matches
bestSentence = sentence
}
}
// Trim to reasonable length
return bestSentence.trim().slice(0, 200) + '...'
}
}
export class RecommendationEngine {
constructor(
private vectorize: VectorizeIndex,
private ai: Ai,
private kv: KVNamespace
) {}
async indexProduct(product: {
id: string
name: string
description: string
category: string
price: number
attributes: Record<string, any>
}): Promise<void> {
// Create rich text representation
const textRepresentation = `
${product.name}
${product.description}
Category: ${product.category}
${Object.entries(product.attributes)
.map(([k, v]) => `${k}: ${v}`)
.join(' ')}
`.trim()
// Generate embedding
const response = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [textRepresentation]
})
// Store in Vectorize
await this.vectorize.insert([{
id: product.id,
values: response.data[0],
metadata: {
name: product.name,
category: product.category,
price: product.price,
...product.attributes
}
}])
}
async getSimilarProducts(
productId: string,
options: {
limit?: number
priceRange?: { min: number; max: number }
category?: string
} = {}
): Promise<Array<{
id: string
name: string
score: number
price: number
}>> {
// Get product vector
const product = await this.vectorize.getByIds([productId])
if (!product || product.length === 0) {
throw new Error('Product not found')
}
// Build filter
const filter: Record<string, any> = {}
if (options.priceRange) {
filter.price = {
$gte: options.priceRange.min,
$lte: options.priceRange.max
}
}
if (options.category) {
filter.category = options.category
}
// Find similar products
const results = await this.vectorize.query(product[0].values, {
topK: (options.limit || 10) + 1, // +1 to exclude self
filter
})
// Filter out the queried product and return
return results.matches
.filter(match => match.id !== productId)
.map(match => ({
id: match.id,
name: match.metadata.name,
score: match.score,
price: match.metadata.price
}))
.slice(0, options.limit || 10)
}
async getPersonalizedRecommendations(
userId: string,
limit: number = 10
): Promise<Array<any>> {
// Get user interaction history
const history = await this.getUserHistory(userId)
if (history.length === 0) {
// Return popular products for new users
return this.getPopularProducts(limit)
}
// Calculate user preference vector
const userVector = await this.calculateUserPreferenceVector(history)
// Find products matching user preferences
const results = await this.vectorize.query(userVector, {
topK: limit * 2, // Get extra to filter
filter: {
id: { $nin: history.map(h => h.productId) } // Exclude already seen
}
})
// Re-rank based on multiple factors
const reranked = await this.rerankResults(results.matches, userId)
return reranked.slice(0, limit)
}
private async calculateUserPreferenceVector(
history: Array<{ productId: string; interaction: string; timestamp: number }>
): Promise<number[]> {
// Get vectors for interacted products
const productIds = history.map(h => h.productId)
const products = await this.vectorize.getByIds(productIds)
// Weight by interaction type and recency
const weights = history.map(h => {
const recencyWeight = Math.exp(-(Date.now() - h.timestamp) / (30 * 24 * 60 * 60 * 1000))
const interactionWeight = {
view: 0.1,
click: 0.3,
purchase: 1.0,
review: 0.8
}[h.interaction] || 0.1
return recencyWeight * interactionWeight
})
// Calculate weighted average
const dimensions = products[0].values.length
const avgVector = new Array(dimensions).fill(0)
for (let i = 0; i < products.length; i++) {
const product = products[i]
const weight = weights[i]
for (let d = 0; d < dimensions; d++) {
avgVector[d] += product.values[d] * weight
}
}
// Normalize
const totalWeight = weights.reduce((sum, w) => sum + w, 0)
return avgVector.map(v => v / totalWeight)
}
private async getUserHistory(userId: string) {
const cached = await this.kv.get(`user_history:${userId}`, 'json')
return cached || []
}
private async getPopularProducts(limit: number) {
// Implementation depends on your tracking
return []
}
private async rerankResults(matches: any[], userId: string) {
// Rerank based on business logic
return matches
}
}
export class MultiModalSearch {
constructor(
private textIndex: VectorizeIndex,
private imageIndex: VectorizeIndex,
private ai: Ai
) {}
async indexContent(content: {
id: string
text?: string
imageUrl?: string
metadata: Record<string, any>
}): Promise<void> {
const vectors: Array<{
index: VectorizeIndex
data: VectorizeVector
}> = []
// Process text if available
if (content.text) {
const textResponse = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [content.text]
})
vectors.push({
index: this.textIndex,
data: {
id: `${content.id}-text`,
values: textResponse.data[0],
metadata: {
contentId: content.id,
type: 'text',
...content.metadata
}
}
})
}
// Process image if available
if (content.imageUrl) {
const imageResponse = await fetch(content.imageUrl)
const imageBlob = await imageResponse.blob()
const imageEmbedding = await this.ai.run('@cf/openai/clip-vit-base-patch32', {
image: Array.from(new Uint8Array(await imageBlob.arrayBuffer()))
})
vectors.push({
index: this.imageIndex,
data: {
id: `${content.id}-image`,
values: imageEmbedding.data[0],
metadata: {
contentId: content.id,
type: 'image',
imageUrl: content.imageUrl,
...content.metadata
}
}
})
}
// Insert all vectors
await Promise.all(
vectors.map(({ index, data }) => index.insert([data]))
)
}
async search(query: {
text?: string
imageUrl?: string
weights?: { text: number; image: number }
}): Promise<Array<{
id: string
score: number
type: string
metadata: any
}>> {
const weights = query.weights || { text: 0.5, image: 0.5 }
const results: Array<any> = []
// Text search
if (query.text) {
const textEmbedding = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [query.text]
})
const textResults = await this.textIndex.query(textEmbedding.data[0], {
topK: 20
})
results.push(...textResults.matches.map(match => ({
...match,
score: match.score * weights.text,
searchType: 'text'
})))
}
// Image search
if (query.imageUrl) {
const imageResponse = await fetch(query.imageUrl)
const imageBlob = await imageResponse.blob()
const imageEmbedding = await this.ai.run('@cf/openai/clip-vit-base-patch32', {
image: Array.from(new Uint8Array(await imageBlob.arrayBuffer()))
})
const imageResults = await this.imageIndex.query(imageEmbedding.data[0], {
topK: 20
})
results.push(...imageResults.matches.map(match => ({
...match,
score: match.score * weights.image,
searchType: 'image'
})))
}
// Combine and deduplicate results
const combined = new Map<string, any>()
for (const result of results) {
const contentId = result.metadata.contentId
if (!combined.has(contentId) || result.score > combined.get(contentId).score) {
combined.set(contentId, {
id: contentId,
score: result.score,
type: result.searchType,
metadata: result.metadata
})
}
}
// Sort by combined score
return Array.from(combined.values())
.sort((a, b) => b.score - a.score)
.slice(0, 10)
}
}
export class QuestionAnsweringSystem {
constructor(
private vectorize: VectorizeIndex,
private ai: Ai,
private kv: KVNamespace
) {}
async answerQuestion(question: string): Promise<{
answer: string
sources: Array<{ title: string; url: string; relevance: number }>
confidence: number
}> {
// Check cache first
const cacheKey = `qa:${await this.hashQuestion(question)}`
const cached = await this.kv.get(cacheKey, 'json')
if (cached) {
return cached
}
// Generate question embedding
const questionEmbedding = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [question]
})
// Find relevant documents
const searchResults = await this.vectorize.query(questionEmbedding.data[0], {
topK: 5,
filter: { type: 'faq' }
})
// Extract context from top matches
const context = searchResults.matches
.map(match => match.metadata.text)
.join('\n\n')
// Generate answer using LLM
const answerResponse = await this.ai.run('@cf/meta/llama-2-7b-chat-int8', {
prompt: `Based on the following context, answer the question concisely and accurately.
Context:
${context}
Question: ${question}
Answer:`,
max_tokens: 150
})
// Calculate confidence based on vector similarity scores
const avgScore = searchResults.matches.reduce((sum, m) => sum + m.score, 0) / searchResults.matches.length
const confidence = Math.min(avgScore * 100, 95)
const result = {
answer: answerResponse.response,
sources: searchResults.matches.map(match => ({
title: match.metadata.title,
url: match.metadata.url,
relevance: match.score
})),
confidence
}
// Cache the result
await this.kv.put(cacheKey, JSON.stringify(result), {
expirationTtl: 3600 // 1 hour
})
return result
}
async indexFAQ(faq: {
question: string
answer: string
category: string
url: string
}): Promise<void> {
// Combine question and answer for richer embedding
const text = `Question: ${faq.question}\nAnswer: ${faq.answer}`
const embedding = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [text]
})
await this.vectorize.insert([{
id: crypto.randomUUID(),
values: embedding.data[0],
metadata: {
type: 'faq',
title: faq.question,
text: faq.answer,
category: faq.category,
url: faq.url
}
}])
}
private async hashQuestion(question: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(question.toLowerCase().trim())
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)))
}
}
export class HybridSearch {
constructor(
private vectorize: VectorizeIndex,
private db: D1Database,
private ai: Ai
) {}
async search(
query: string,
options: {
vectorWeight?: number
keywordWeight?: number
limit?: number
} = {}
): Promise<Array<{
id: string
score: number
title: string
excerpt: string
}>> {
const vectorWeight = options.vectorWeight || 0.7
const keywordWeight = options.keywordWeight || 0.3
// Parallel searches
const [vectorResults, keywordResults] = await Promise.all([
this.vectorSearch(query, options.limit || 20),
this.keywordSearch(query, options.limit || 20)
])
// Combine scores
const combined = new Map<string, any>()
// Add vector results
for (const result of vectorResults) {
combined.set(result.id, {
...result,
vectorScore: result.score * vectorWeight,
keywordScore: 0,
finalScore: result.score * vectorWeight
})
}
// Add keyword results
for (const result of keywordResults) {
if (combined.has(result.id)) {
const existing = combined.get(result.id)
existing.keywordScore = result.score * keywordWeight
existing.finalScore = existing.vectorScore + existing.keywordScore
} else {
combined.set(result.id, {
...result,
vectorScore: 0,
keywordScore: result.score * keywordWeight,
finalScore: result.score * keywordWeight
})
}
}
// Sort by combined score
return Array.from(combined.values())
.sort((a, b) => b.finalScore - a.finalScore)
.slice(0, options.limit || 10)
}
private async vectorSearch(query: string, limit: number) {
const embedding = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [query]
})
const results = await this.vectorize.query(embedding.data[0], {
topK: limit
})
return results.matches.map(match => ({
id: match.id,
score: match.score,
title: match.metadata.title,
excerpt: match.metadata.excerpt
}))
}
private async keywordSearch(query: string, limit: number) {
const results = await this.db.prepare(`
SELECT id, title, excerpt,
(
(CASE WHEN title LIKE ? THEN 10 ELSE 0 END) +
(CASE WHEN excerpt LIKE ? THEN 5 ELSE 0 END) +
(LENGTH(title) - LENGTH(REPLACE(LOWER(title), LOWER(?), ''))) +
(LENGTH(excerpt) - LENGTH(REPLACE(LOWER(excerpt), LOWER(?), '')))
) as score
FROM documents
WHERE title LIKE ? OR excerpt LIKE ?
ORDER BY score DESC
LIMIT ?
`).bind(
`%${query}%`, `%${query}%`,
query, query,
`%${query}%`, `%${query}%`,
limit
).all()
return results.results.map(row => ({
id: row.id,
score: row.score / 100, // Normalize
title: row.title,
excerpt: row.excerpt
}))
}
}
export class TimeAwareVectorSearch {
constructor(private vectorize: VectorizeIndex, private ai: Ai) {}
async search(
query: string,
options: {
timeDecay?: number
halfLife?: number
limit?: number
} = {}
): Promise<Array<any>> {
const embedding = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: [query]
})
const results = await this.vectorize.query(embedding.data[0], {
topK: (options.limit || 10) * 3 // Get extra for re-ranking
})
const now = Date.now()
const halfLife = options.halfLife || 30 * 24 * 60 * 60 * 1000 // 30 days
const timeDecay = options.timeDecay || 0.3
// Re-rank with time decay
const reranked = results.matches.map(match => {
const age = now - new Date(match.metadata.publishedAt).getTime()
const decayFactor = Math.exp(-age / halfLife)
return {
...match,
originalScore: match.score,
timeBoost: decayFactor * timeDecay,
finalScore: match.score * (1 - timeDecay) + decayFactor * timeDecay
}
})
return reranked
.sort((a, b) => b.finalScore - a.finalScore)
.slice(0, options.limit || 10)
}
}
export class BatchVectorProcessor {
private queue: Array<{
text: string
resolve: (embedding: number[]) => void
reject: (error: any) => void
}> = []
constructor(
private ai: Ai,
private batchSize: number = 10,
private delayMs: number = 100
) {
this.processBatch()
}
async getEmbedding(text: string): Promise<number[]> {
return new Promise((resolve, reject) => {
this.queue.push({ text, resolve, reject })
})
}
private async processBatch() {
while (true) {
await new Promise(resolve => setTimeout(resolve, this.delayMs))
if (this.queue.length === 0) continue
const batch = this.queue.splice(0, this.batchSize)
try {
const response = await this.ai.run('@cf/baai/bge-base-en-v1.5', {
text: batch.map(item => item.text)
})
batch.forEach((item, index) => {
item.resolve(response.data[index])
})
} catch (error) {
batch.forEach(item => item.reject(error))
}
}
}
}
export class DimensionReducer {
async reduceDimensions(
vectors: number[][],
targetDimensions: number
): Promise<number[][]> {
// Simple PCA-like reduction
const means = this.calculateMeans(vectors)
const centered = vectors.map(v =>
v.map((val, i) => val - means[i])
)
// Calculate covariance matrix (simplified)
const covariance = this.calculateCovariance(centered)
// Get top eigenvectors (simplified - use proper library in production)
const topComponents = this.getTopComponents(covariance, targetDimensions)
// Project vectors
return centered.map(vector =>
this.projectVector(vector, topComponents)
)
}
private calculateMeans(vectors: number[][]): number[] {
const dims = vectors[0].length
const means = new Array(dims).fill(0)
for (const vector of vectors) {
for (let i = 0; i < dims; i++) {
means[i] += vector[i]
}
}
return means.map(sum => sum / vectors.length)
}
private calculateCovariance(vectors: number[][]): number[][] {
// Simplified - implement proper covariance calculation
return []
}
private getTopComponents(covariance: number[][], k: number): number[][] {
// Simplified - implement proper eigenvalue decomposition
return []
}
private projectVector(vector: number[], components: number[][]): number[] {
return components.map(component =>
vector.reduce((sum, val, i) => sum + val * component[i], 0)
)
}
}
const vectorizeLimits = {
vectors: "5 million on free tier",
dimensions: "1536 maximum",
queries: "30,000/month free",
indexSize: "50GB maximum",
metadataSize: "10KB per vector",
batchInsert: "1000 vectors per request"
}
const bestPractices = {
chunking: "Split large documents into smaller chunks",
metadata: "Store searchable attributes in metadata",
filtering: "Use metadata filters to narrow search space",
normalization: "Normalize vectors for cosine similarity",
caching: "Cache frequently searched queries",
monitoring: "Track query performance and optimize"
}
Vectorize transforms how we build search and recommendation features by understanding semantic meaning rather than just matching keywords. With 5 million vectors and 30,000 queries monthly on the free tier, it enables sophisticated AI-powered features that would typically require expensive infrastructure. Vectorize is essential for semantic search, recommendation engines, question-answering systems, and any application that benefits from understanding content similarity.
Next: Workers AI - LLMs and image generation at the edge
The Pitch: Workers AI gives you instant access to 40+ AI models including LLMs, image generation, and speech recognition with zero infrastructure setup.
The Win:
// Generate text with Llama 2 in one line const result = await env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Explain quantum computing in simple terms', max_tokens: 200 })The Catch: 10,000 neurons/day (about 200-500 LLM requests) - enough for features and demos, but you'll need careful usage tracking for production apps.
TL;DR - Key Takeaways
- What: Run AI models at the edge without managing infrastructure
- Free Tier: 10,000 neurons/day (inference units)
- Primary Use Cases: Text generation, image creation, embeddings, speech-to-text
- Key Features: 40+ models available, automatic scaling, global inference
- Limitations: Request size limits vary by model, no fine-tuning on free tier
Run LLMs, generate images, and process speech—all without managing GPU infrastructure. Workers AI provides 40+ models accessible via simple API calls, with 10,000 inference units daily on the free tier.
// AI infrastructure comparison
const aiComparison = {
traditionalAI: {
setup: "Provision GPU servers ($1000s/month)",
latency: "100-500ms from distant regions",
scaling: "Manual, complex, expensive",
models: "Self-hosted, self-managed"
},
workersAI: {
setup: "Just call the API",
latency: "Sub-100ms globally",
scaling: "Automatic, unlimited",
models: "30+ models ready to use"
}
}
interface Env {
AI: Ai
}
export default {
async fetch(request: Request, env: Env) {
// Text generation
const response = await env.AI.run('@cf/meta/llama-2-7b-chat-int8', {
prompt: 'Write a haiku about cloudflare',
max_tokens: 100
})
return Response.json({
haiku: response.response
})
}
}
Workers AI offers models across multiple categories:
const availableModels = {
// Text Generation
llms: [
'@cf/meta/llama-2-7b-chat-int8', // General chat
'@cf/mistral/mistral-7b-instruct-v0.1', // Instruction following
'@cf/microsoft/phi-2', // Code generation
],
// Text Classification & Embeddings
understanding: [
'@cf/baai/bge-base-en-v1.5', // Text embeddings
'@cf/huggingface/distilbert-sst-2-int8', // Sentiment
],
// Image Generation
imageGen: [
'@cf/stabilityai/stable-diffusion-xl-base-1.0',
'@cf/lykon/dreamshaper-8-lcm',
],
// Image Analysis
vision: [
'@cf/openai/clip-vit-base-patch32', // Image embeddings
'@cf/microsoft/resnet-50', // Classification
],
// Speech
audio: [
'@cf/openai/whisper', // Speech to text
],
// Translation
translation: [
'@cf/meta/m2m100-1.2b', // 100+ languages
]
}
export class ChatBot {
private conversationHistory: Map<string, Array<{
role: string
content: string
}>> = new Map()
constructor(
private ai: Ai,
private kv: KVNamespace
) {}
async chat(
sessionId: string,
message: string,
options: {
model?: string
temperature?: number
maxTokens?: number
systemPrompt?: string
} = {}
): Promise<{
response: string
usage: {
promptTokens: number
completionTokens: number
totalTokens: number
}
}> {
// Get conversation history
let history = await this.getHistory(sessionId)
// Add user message
history.push({ role: 'user', content: message })
// Build prompt
const prompt = this.buildPrompt(history, options.systemPrompt)
// Generate response
const startTime = Date.now()
const response = await this.ai.run(
options.model || '@cf/meta/llama-2-7b-chat-int8',
{
prompt,
max_tokens: options.maxTokens || 500,
temperature: options.temperature || 0.7,
stream: false
}
)
// Add assistant response to history
history.push({ role: 'assistant', content: response.response })
// Trim history if too long
if (history.length > 20) {
history = history.slice(-20)
}
// Save history
await this.saveHistory(sessionId, history)
// Calculate token usage (approximate)
const usage = {
promptTokens: Math.ceil(prompt.length / 4),
completionTokens: Math.ceil(response.response.length / 4),
totalTokens: Math.ceil((prompt.length + response.response.length) / 4)
}
// Log metrics
await this.logMetrics(sessionId, {
model: options.model || '@cf/meta/llama-2-7b-chat-int8',
latency: Date.now() - startTime,
tokens: usage.totalTokens
})
return {
response: response.response,
usage
}
}
private buildPrompt(
history: Array<{ role: string; content: string }>,
systemPrompt?: string
): string {
const system = systemPrompt || 'You are a helpful AI assistant.'
let prompt = `System: ${system}\n\n`
for (const message of history) {
prompt += `${message.role === 'user' ? 'Human' : 'Assistant'}: ${message.content}\n\n`
}
prompt += 'Assistant: '
return prompt
}
private async getHistory(sessionId: string) {
const cached = await this.kv.get(`chat:${sessionId}`, 'json')
return cached || []
}
private async saveHistory(sessionId: string, history: any[]) {
await this.kv.put(`chat:${sessionId}`, JSON.stringify(history), {
expirationTtl: 3600 // 1 hour
})
}
private async logMetrics(sessionId: string, metrics: any) {
// Log to analytics or monitoring service
}
}
// Worker implementation
export default {
async fetch(request: Request, env: Env) {
const chatbot = new ChatBot(env.AI, env.KV)
if (request.method === 'POST' && request.url.includes('/chat')) {
const { sessionId, message, options } = await request.json()
const response = await chatbot.chat(sessionId, message, options)
return Response.json(response)
}
return new Response('Chat API', { status: 200 })
}
}
export class ContentModerator {
constructor(
private ai: Ai,
private db: D1Database
) {}
async moderateContent(content: {
id: string
text?: string
imageUrl?: string
userId: string
}): Promise<{
approved: boolean
reasons: string[]
scores: Record<string, number>
actions: string[]
}> {
const scores: Record<string, number> = {}
const reasons: string[] = []
const actions: string[] = []
// Text moderation
if (content.text) {
const textResults = await this.moderateText(content.text)
Object.assign(scores, textResults.scores)
reasons.push(...textResults.reasons)
}
// Image moderation
if (content.imageUrl) {
const imageResults = await this.moderateImage(content.imageUrl)
Object.assign(scores, imageResults.scores)
reasons.push(...imageResults.reasons)
}
// Determine approval
const approved = this.determineApproval(scores)
// Determine actions
if (!approved) {
if (scores.toxicity > 0.9 || scores.nsfw > 0.9) {
actions.push('ban_user')
} else if (scores.toxicity > 0.7 || scores.nsfw > 0.7) {
actions.push('warn_user')
}
actions.push('hide_content')
}
// Log moderation result
await this.logModeration({
contentId: content.id,
userId: content.userId,
approved,
scores,
reasons,
actions,
timestamp: Date.now()
})
return { approved, reasons, scores, actions }
}
private async moderateText(text: string): Promise<{
scores: Record<string, number>
reasons: string[]
}> {
const scores: Record<string, number> = {}
const reasons: string[] = []
// Sentiment analysis
const sentiment = await this.ai.run(
'@cf/huggingface/distilbert-sst-2-int8',
{ text }
)
scores.negativity = sentiment[0].score
// Toxicity detection (using LLM)
const toxicityPrompt = `Rate the toxicity of this text from 0 to 1, where 0 is not toxic and 1 is very toxic. Only respond with a number.
Text: ${text}
Rating:`
const toxicityResponse = await this.ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt: toxicityPrompt,
max_tokens: 10
}
)
scores.toxicity = parseFloat(toxicityResponse.response) || 0
if (scores.toxicity > 0.7) {
reasons.push('High toxicity detected')
}
// Spam detection
const spamKeywords = ['buy now', 'click here', 'limited offer', 'act now']
const spamScore = spamKeywords.filter(keyword =>
text.toLowerCase().includes(keyword)
).length / spamKeywords.length
scores.spam = spamScore
if (scores.spam > 0.5) {
reasons.push('Spam content detected')
}
return { scores, reasons }
}
private async moderateImage(imageUrl: string): Promise<{
scores: Record<string, number>
reasons: string[]
}> {
const scores: Record<string, number> = {}
const reasons: string[] = []
// Fetch image
const response = await fetch(imageUrl)
const imageBlob = await response.blob()
const imageArray = new Uint8Array(await imageBlob.arrayBuffer())
// NSFW detection (using image classification)
const classification = await this.ai.run(
'@cf/microsoft/resnet-50',
{ image: Array.from(imageArray) }
)
// Check for NSFW categories
const nsfwCategories = ['nude', 'adult', 'explicit']
let nsfwScore = 0
for (const result of classification) {
if (nsfwCategories.some(cat => result.label.toLowerCase().includes(cat))) {
nsfwScore = Math.max(nsfwScore, result.score)
}
}
scores.nsfw = nsfwScore
if (scores.nsfw > 0.7) {
reasons.push('NSFW content detected')
}
return { scores, reasons }
}
private determineApproval(scores: Record<string, number>): boolean {
return scores.toxicity < 0.7 &&
scores.nsfw < 0.7 &&
scores.spam < 0.7
}
private async logModeration(result: any) {
await this.db.prepare(`
INSERT INTO moderation_log
(content_id, user_id, approved, scores, reasons, actions, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).bind(
result.contentId,
result.userId,
result.approved ? 1 : 0,
JSON.stringify(result.scores),
JSON.stringify(result.reasons),
JSON.stringify(result.actions),
result.timestamp
).run()
}
}
export class ImageGenerator {
constructor(
private ai: Ai,
private r2: R2Bucket,
private kv: KVNamespace
) {}
async generateImage(params: {
prompt: string
negativePrompt?: string
style?: string
width?: number
height?: number
userId: string
}): Promise<{
url: string
id: string
metadata: Record<string, any>
}> {
// Check rate limit
const rateLimitKey = `image_gen:${params.userId}`
const dailyCount = parseInt(await this.kv.get(rateLimitKey) || '0')
if (dailyCount >= 50) { // 50 images per day per user
throw new Error('Daily generation limit exceeded')
}
// Enhance prompt based on style
const enhancedPrompt = this.enhancePrompt(params.prompt, params.style)
// Generate image
const response = await this.ai.run(
'@cf/stabilityai/stable-diffusion-xl-base-1.0',
{
prompt: enhancedPrompt,
negative_prompt: params.negativePrompt,
width: params.width || 1024,
height: params.height || 1024,
num_steps: 20
}
)
// Save to R2
const imageId = crypto.randomUUID()
const key = `generated/${params.userId}/${imageId}.png`
await this.r2.put(key, response.image, {
httpMetadata: {
contentType: 'image/png'
},
customMetadata: {
prompt: params.prompt,
style: params.style || 'default',
userId: params.userId,
generatedAt: new Date().toISOString()
}
})
// Update rate limit
await this.kv.put(rateLimitKey, (dailyCount + 1).toString(), {
expirationTtl: 86400 // 24 hours
})
// Log generation
await this.logGeneration({
imageId,
userId: params.userId,
prompt: params.prompt,
style: params.style,
timestamp: Date.now()
})
return {
url: `/images/${key}`,
id: imageId,
metadata: {
prompt: params.prompt,
style: params.style,
dimensions: `${params.width || 1024}x${params.height || 1024}`
}
}
}
private enhancePrompt(prompt: string, style?: string): string {
const styleEnhancements: Record<string, string> = {
'photorealistic': 'photorealistic, high detail, professional photography',
'anime': 'anime style, manga, cel-shaded',
'oil-painting': 'oil painting, artistic, textured brushstrokes',
'watercolor': 'watercolor painting, soft colors, artistic',
'digital-art': 'digital art, concept art, highly detailed',
'sketch': 'pencil sketch, hand-drawn, artistic'
}
const enhancement = styleEnhancements[style || 'default'] || ''
return enhancement ? `${prompt}, ${enhancement}` : prompt
}
async generateVariations(
originalImageId: string,
count: number = 4
): Promise<Array<{
url: string
id: string
}>> {
// Get original metadata
const originalKey = await this.findImageKey(originalImageId)
const original = await this.r2.get(originalKey)
if (!original) {
throw new Error('Original image not found')
}
const metadata = original.customMetadata
const variations = []
// Generate variations with slight prompt modifications
for (let i = 0; i < count; i++) {
const variedPrompt = `${metadata.prompt}, variation ${i + 1}, slightly different`
const result = await this.generateImage({
prompt: variedPrompt,
style: metadata.style,
userId: metadata.userId
})
variations.push(result)
}
return variations
}
private async findImageKey(imageId: string): Promise<string> {
// Implementation depends on your storage structure
return `generated/${imageId}.png`
}
private async logGeneration(data: any) {
// Log to analytics or database
}
}
export class DocumentIntelligence {
constructor(
private ai: Ai,
private vectorize: VectorizeIndex
) {}
async processDocument(document: {
id: string
content: string
type: 'email' | 'report' | 'article' | 'contract'
}): Promise<{
summary: string
keyPoints: string[]
entities: Array<{ type: string; value: string }>
sentiment: string
category: string
actionItems: string[]
}> {
// Generate summary
const summaryPrompt = `Summarize this ${document.type} in 2-3 sentences:
${document.content}
Summary:`
const summaryResponse = await this.ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt: summaryPrompt,
max_tokens: 150
}
)
// Extract key points
const keyPointsPrompt = `List the 3-5 most important points from this ${document.type}:
${document.content}
Key points:`
const keyPointsResponse = await this.ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt: keyPointsPrompt,
max_tokens: 200
}
)
// Extract entities (simplified - use NER model in production)
const entities = this.extractEntities(document.content)
// Analyze sentiment
const sentiment = await this.analyzeSentiment(document.content)
// Categorize document
const category = await this.categorizeDocument(document)
// Extract action items
const actionItems = await this.extractActionItems(document)
// Store embeddings for future search
await this.storeEmbeddings(document)
return {
summary: summaryResponse.response,
keyPoints: this.parseKeyPoints(keyPointsResponse.response),
entities,
sentiment,
category,
actionItems
}
}
private extractEntities(content: string): Array<{ type: string; value: string }> {
const entities = []
// Email regex
const emails = content.match(/[\w.-]+@[\w.-]+\.\w+/g) || []
entities.push(...emails.map(email => ({ type: 'email', value: email })))
// Phone regex (simplified)
const phones = content.match(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g) || []
entities.push(...phones.map(phone => ({ type: 'phone', value: phone })))
// Date regex (simplified)
const dates = content.match(/\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/g) || []
entities.push(...dates.map(date => ({ type: 'date', value: date })))
// Money amounts
const amounts = content.match(/\$[\d,]+\.?\d*/g) || []
entities.push(...amounts.map(amount => ({ type: 'money', value: amount })))
return entities
}
private async analyzeSentiment(content: string): Promise<string> {
const response = await this.ai.run(
'@cf/huggingface/distilbert-sst-2-int8',
{ text: content.slice(0, 512) } // Model has token limit
)
const score = response[0].score
if (score > 0.8) return 'positive'
if (score > 0.6) return 'slightly positive'
if (score > 0.4) return 'neutral'
if (score > 0.2) return 'slightly negative'
return 'negative'
}
private async categorizeDocument(document: any): Promise<string> {
const categories = {
email: ['personal', 'business', 'marketing', 'support'],
report: ['financial', 'technical', 'research', 'status'],
article: ['news', 'tutorial', 'opinion', 'review'],
contract: ['employment', 'service', 'lease', 'purchase']
}
const availableCategories = categories[document.type] || ['general']
const prompt = `Categorize this ${document.type} into one of these categories: ${availableCategories.join(', ')}
Content: ${document.content.slice(0, 1000)}
Category:`
const response = await this.ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt,
max_tokens: 20
}
)
return response.response.trim().toLowerCase()
}
private async extractActionItems(document: any): Promise<string[]> {
if (!['email', 'report'].includes(document.type)) {
return []
}
const prompt = `Extract any action items or tasks from this ${document.type}:
${document.content}
Action items (list each on a new line):`
const response = await this.ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt,
max_tokens: 200
}
)
return response.response
.split('\n')
.filter(line => line.trim())
.map(line => line.replace(/^[-*]\s*/, ''))
}
private parseKeyPoints(response: string): string[] {
return response
.split('\n')
.filter(line => line.trim())
.map(line => line.replace(/^\d+\.\s*/, ''))
.slice(0, 5)
}
private async storeEmbeddings(document: any) {
const embedding = await this.ai.run(
'@cf/baai/bge-base-en-v1.5',
{ text: [document.content.slice(0, 1000)] }
)
await this.vectorize.insert([{
id: document.id,
values: embedding.data[0],
metadata: {
type: document.type,
summary: document.summary,
timestamp: Date.now()
}
}])
}
}
export class StreamingAI {
async streamCompletion(
ai: Ai,
prompt: string
): Promise<ReadableStream> {
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
const encoder = new TextEncoder()
// Start generation in background
(async () => {
try {
const response = await ai.run(
'@cf/meta/llama-2-7b-chat-int8',
{
prompt,
stream: true,
max_tokens: 500
}
)
// Stream tokens as they arrive
for await (const chunk of response) {
const text = chunk.response || chunk.text || ''
await writer.write(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`))
}
await writer.write(encoder.encode('data: [DONE]\n\n'))
} catch (error) {
await writer.write(encoder.encode(`data: ${JSON.stringify({ error: error.message })}\n\n`))
} finally {
await writer.close()
}
})()
return readable
}
}
// Usage in Worker
export default {
async fetch(request: Request, env: Env) {
if (request.url.includes('/stream')) {
const { prompt } = await request.json()
const streamer = new StreamingAI()
const stream = await streamer.streamCompletion(env.AI, prompt)
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})
}
}
}
export class ModelRouter {
private modelCapabilities = {
'@cf/meta/llama-2-7b-chat-int8': {
strengths: ['general', 'creative', 'conversation'],
maxTokens: 2048,
speed: 'fast'
},
'@cf/mistral/mistral-7b-instruct-v0.1': {
strengths: ['instruction', 'technical', 'analysis'],
maxTokens: 4096,
speed: 'medium'
},
'@cf/microsoft/phi-2': {
strengths: ['code', 'math', 'reasoning'],
maxTokens: 2048,
speed: 'very-fast'
}
}
selectModel(task: {
type: string
complexity: 'low' | 'medium' | 'high'
responseLength: number
}): string {
// Route based on task type
if (task.type === 'code') {
return '@cf/microsoft/phi-2'
}
if (task.type === 'creative' || task.type === 'conversation') {
return '@cf/meta/llama-2-7b-chat-int8'
}
if (task.type === 'analysis' || task.complexity === 'high') {
return '@cf/mistral/mistral-7b-instruct-v0.1'
}
// Default based on response length
if (task.responseLength > 1000) {
return '@cf/mistral/mistral-7b-instruct-v0.1'
}
return '@cf/meta/llama-2-7b-chat-int8'
}
}
export class AIOptimizer {
constructor(
private ai: Ai,
private kv: KVNamespace
) {}
async optimizedInference(params: {
prompt: string
maxTokens: number
cacheKey?: string
cacheTtl?: number
}): Promise<string> {
// Check cache first
if (params.cacheKey) {
const cached = await this.kv.get(params.cacheKey)
if (cached) {
return cached
}
}
// Use smaller model for simple tasks
const complexity = this.assessComplexity(params.prompt)
const model = complexity === 'low'
? '@cf/microsoft/phi-2'
: '@cf/meta/llama-2-7b-chat-int8'
// Optimize token usage
const optimizedPrompt = this.optimizePrompt(params.prompt)
// Run inference
const response = await this.ai.run(model, {
prompt: optimizedPrompt,
max_tokens: Math.min(params.maxTokens, 500), // Cap tokens
temperature: 0.7
})
// Cache result
if (params.cacheKey) {
await this.kv.put(
params.cacheKey,
response.response,
{ expirationTtl: params.cacheTtl || 3600 }
)
}
return response.response
}
private assessComplexity(prompt: string): 'low' | 'high' {
const wordCount = prompt.split(/\s+/).length
const hasCode = /```|function|class|def|import/.test(prompt)
const hasAnalysis = /analyze|explain|compare|evaluate/.test(prompt.toLowerCase())
if (wordCount > 200 || hasCode || hasAnalysis) {
return 'high'
}
return 'low'
}
private optimizePrompt(prompt: string): string {
// Remove redundant whitespace
let optimized = prompt.replace(/\s+/g, ' ').trim()
// Remove unnecessary examples if prompt is long
if (optimized.length > 1000) {
optimized = optimized.replace(/For example:.*?(?=\n\n|\n[A-Z]|$)/gs, '')
}
return optimized
}
}
export class AIMonitor {
constructor(
private analytics: AnalyticsEngineDataset,
private kv: KVNamespace
) {}
async trackInference(params: {
model: string
promptLength: number
responseLength: number
latency: number
userId: string
success: boolean
}) {
// Log to analytics
await this.analytics.writeDataPoint({
dataset: 'ai_metrics',
point: {
model: params.model,
promptTokens: Math.ceil(params.promptLength / 4),
completionTokens: Math.ceil(params.responseLength / 4),
latency: params.latency,
userId: params.userId,
success: params.success ? 1 : 0,
timestamp: Date.now()
}
})
// Update daily usage
const dayKey = `usage:${new Date().toISOString().split('T')[0]}`
const usage = await this.kv.get(dayKey, 'json') || {}
usage[params.userId] = (usage[params.userId] || 0) + 1
await this.kv.put(dayKey, JSON.stringify(usage), {
expirationTtl: 86400 * 7 // Keep for 7 days
})
}
async getUserUsage(userId: string): Promise<{
daily: number
weekly: number
models: Record<string, number>
}> {
const today = new Date().toISOString().split('T')[0]
const daily = await this.kv.get(`usage:${today}`, 'json') || {}
// Calculate weekly
let weekly = 0
for (let i = 0; i < 7; i++) {
const date = new Date()
date.setDate(date.getDate() - i)
const dayKey = `usage:${date.toISOString().split('T')[0]}`
const dayUsage = await this.kv.get(dayKey, 'json') || {}
weekly += dayUsage[userId] || 0
}
return {
daily: daily[userId] || 0,
weekly,
models: {} // Would need separate tracking
}
}
}
const workersAILimits = {
freeNeurons: "10,000 per day",
models: "30+ available models",
requestSize: "100KB max input",
responseSize: "Varies by model",
concurrency: "50 simultaneous requests",
timeout: "60 seconds per request"
}
const neuronCosts = {
// Approximate neurons per operation
textGeneration: {
small: 5, // < 100 tokens
medium: 20, // 100-500 tokens
large: 50 // 500+ tokens
},
imageGeneration: {
standard: 100, // 512x512
hd: 500 // 1024x1024
},
embeddings: 1,
classification: 1,
speechToText: 30
}
const bestPractices = {
caching: "Cache AI responses when possible",
batching: "Batch similar requests together",
modelSelection: "Choose appropriate model for task",
promptEngineering: "Optimize prompts for clarity and brevity",
errorHandling: "Implement fallbacks for quota limits",
monitoring: "Track usage to stay within limits"
}
Workers AI brings the power of large language models, image generation, and other AI capabilities directly to the edge. With 10,000 neurons daily on the free tier, it's enough to add intelligent features to any application without the complexity and cost of managing AI infrastructure. Workers AI enables chatbots, content moderation, image generation, document intelligence, and any feature that benefits from AI inference at the edge.
Next: The Hono Framework - Building modern APIs on Workers
While Pages, Workers, D1, KV, R2, Vectorize, and Workers AI form the foundation of your $0 infrastructure, Cloudflare offers additional services that complete a production-ready stack. These services handle specific use cases that round out your application's capabilities.
Queues enables asynchronous processing with at-least-once delivery guarantees. Perfect for background jobs, webhooks, and event-driven architectures.
// Producer
export default {
async fetch(request: Request, env: Env) {
// Add message to queue
await env.QUEUE.send({
type: 'process_upload',
fileId: 'abc123',
userId: 'user456',
timestamp: Date.now()
})
return Response.json({ status: 'queued' })
}
}
// Consumer
export default {
async queue(batch: MessageBatch<any>, env: Env) {
for (const message of batch.messages) {
try {
await processMessage(message.body, env)
message.ack() // Acknowledge successful processing
} catch (error) {
message.retry() // Retry later
}
}
}
}
// Email notification system
export class NotificationQueue {
async sendEmail(env: Env, email: {
to: string
subject: string
template: string
data: Record<string, any>
}) {
await env.EMAIL_QUEUE.send({
...email,
attemptCount: 0,
maxAttempts: 3
})
}
}
// Image processing pipeline
export class ImageProcessor {
async processUpload(env: Env, upload: {
key: string
userId: string
}) {
// Queue multiple processing tasks
await env.QUEUE.send([
{ task: 'generate_thumbnail', ...upload },
{ task: 'extract_metadata', ...upload },
{ task: 'scan_content', ...upload },
{ task: 'index_search', ...upload }
])
}
}
// Webhook delivery with retries
export class WebhookDelivery {
async queue(batch: MessageBatch<any>, env: Env) {
for (const message of batch.messages) {
const webhook = message.body
try {
const response = await fetch(webhook.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(webhook.payload)
})
if (response.ok) {
message.ack()
} else if (webhook.attemptCount < 5) {
// Exponential backoff
const delay = Math.pow(2, webhook.attemptCount) * 1000
message.retry({ delaySeconds: delay / 1000 })
} else {
// Max attempts reached, dead letter
await env.DEAD_LETTER.send(webhook)
message.ack()
}
} catch (error) {
message.retry()
}
}
}
}
Durable Objects provide strong consistency and real-time coordination. Each object is a single-threaded JavaScript environment with persistent state.
// Chat room implementation
export class ChatRoom {
state: DurableObjectState
sessions: Set<WebSocket> = new Set()
messages: Array<any> = []
constructor(state: DurableObjectState) {
this.state = state
}
async fetch(request: Request) {
const url = new URL(request.url)
if (url.pathname === '/websocket') {
return this.handleWebSocket(request)
}
if (url.pathname === '/history') {
return Response.json({ messages: this.messages })
}
}
async handleWebSocket(request: Request) {
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
this.state.acceptWebSocket(server)
this.sessions.add(server)
server.addEventListener('message', async (event) => {
const message = JSON.parse(event.data)
// Store message
this.messages.push({
...message,
timestamp: Date.now()
})
// Keep last 100 messages
if (this.messages.length > 100) {
this.messages = this.messages.slice(-100)
}
// Broadcast to all connected clients
const broadcast = JSON.stringify(message)
for (const session of this.sessions) {
try {
session.send(broadcast)
} catch (error) {
// Remove dead connections
this.sessions.delete(session)
}
}
})
server.addEventListener('close', () => {
this.sessions.delete(server)
})
return new Response(null, { status: 101, webSocket: client })
}
}
// Collaborative editor
export class DocumentEditor {
state: DurableObjectState
document: { content: string; version: number }
activeUsers: Map<string, { cursor: number; selection: any }>
constructor(state: DurableObjectState) {
this.state = state
this.state.blockConcurrencyWhile(async () => {
this.document = await this.state.storage.get('document') || {
content: '',
version: 0
}
this.activeUsers = new Map()
})
}
async applyOperation(operation: {
type: 'insert' | 'delete'
position: number
text?: string
length?: number
userId: string
}) {
// Apply operational transform
if (operation.type === 'insert') {
this.document.content =
this.document.content.slice(0, operation.position) +
operation.text +
this.document.content.slice(operation.position)
} else {
this.document.content =
this.document.content.slice(0, operation.position) +
this.document.content.slice(operation.position + operation.length!)
}
this.document.version++
// Persist state
await this.state.storage.put('document', this.document)
// Broadcast to all users
this.broadcast({
type: 'operation',
operation,
version: this.document.version
})
}
}
Email Routing lets you process incoming emails with Workers, enabling email-to-webhook, automated responses, and email parsing.
export default {
async email(message: ForwardableEmailMessage, env: Env) {
// Parse email
const subject = message.headers.get('subject')
const from = message.from
// Route based on address
if (message.to.includes('support@')) {
// Create support ticket
await env.DB.prepare(`
INSERT INTO tickets (email, subject, body, status)
VALUES (?, ?, ?, 'open')
`).bind(from, subject, await message.text()).run()
// Auto-reply
await message.reply({
html: `<p>Thank you for contacting support. Ticket created.</p>`
})
}
if (message.to.includes('upload@')) {
// Process attachments
for (const attachment of message.attachments) {
const key = `email-attachments/${Date.now()}-${attachment.name}`
await env.BUCKET.put(key, attachment.content)
}
}
// Forward to webhook
await fetch('https://api.example.com/email-webhook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from,
to: message.to,
subject,
text: await message.text(),
attachments: message.attachments.length
})
})
}
}
Analytics Engine provides privacy-first analytics with SQL querying capabilities.
export class Analytics {
constructor(private analytics: AnalyticsEngineDataset) {}
async trackPageView(request: Request, response: Response) {
const url = new URL(request.url)
await this.analytics.writeDataPoint({
dataset: 'web_analytics',
point: {
// Dimensions
path: url.pathname,
method: request.method,
country: request.cf?.country || 'unknown',
device: this.getDeviceType(request),
// Metrics
status: response.status,
responseTime: Date.now(),
// Privacy-first: no cookies or IPs
timestamp: Date.now()
}
})
}
async trackCustomEvent(event: {
category: string
action: string
label?: string
value?: number
userId?: string
}) {
await this.analytics.writeDataPoint({
dataset: 'events',
point: {
category: event.category,
action: event.action,
label: event.label || '',
value: event.value || 0,
userId: event.userId ? this.hashUserId(event.userId) : 'anonymous',
timestamp: Date.now()
}
})
}
private getDeviceType(request: Request): string {
const ua = request.headers.get('user-agent') || ''
if (/mobile/i.test(ua)) return 'mobile'
if (/tablet/i.test(ua)) return 'tablet'
return 'desktop'
}
private hashUserId(userId: string): string {
// One-way hash for privacy
return btoa(userId).slice(0, 16)
}
}
// Query analytics with SQL
const query = `
SELECT
path,
COUNT(*) as views,
COUNT(DISTINCT session_id) as unique_visitors,
AVG(response_time) as avg_load_time
FROM web_analytics
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY path
ORDER BY views DESC
LIMIT 10
`
Browser Rendering API allows you to capture screenshots and generate PDFs using headless Chrome.
export class BrowserRenderer {
async screenshot(url: string, options: {
viewport?: { width: number; height: number }
fullPage?: boolean
quality?: number
} = {}): Promise<Blob> {
const browser = await puppeteer.launch()
const page = await browser.newPage()
if (options.viewport) {
await page.setViewport(options.viewport)
}
await page.goto(url, { waitUntil: 'networkidle0' })
const screenshot = await page.screenshot({
fullPage: options.fullPage || false,
type: 'jpeg',
quality: options.quality || 80
})
await browser.close()
return new Blob([screenshot], { type: 'image/jpeg' })
}
async generatePDF(html: string, options: {
format?: 'A4' | 'Letter'
margin?: { top: string; bottom: string; left: string; right: string }
} = {}): Promise<Blob> {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setContent(html, { waitUntil: 'networkidle0' })
const pdf = await page.pdf({
format: options.format || 'A4',
margin: options.margin || {
top: '1in',
bottom: '1in',
left: '1in',
right: '1in'
}
})
await browser.close()
return new Blob([pdf], { type: 'application/pdf' })
}
}
// Invoice generation example
export async function generateInvoice(env: Env, invoice: any) {
const renderer = new BrowserRenderer()
const html = `
<!DOCTYPE html>
<html>
<head>
<style>
/* Invoice styles */
</style>
</head>
<body>
<h1>Invoice #${invoice.number}</h1>
<!-- Invoice content -->
</body>
</html>
`
const pdf = await renderer.generatePDF(html)
// Store in R2
await env.BUCKET.put(`invoices/${invoice.number}.pdf`, pdf)
return pdf
}
Hyperdrive provides connection pooling for external databases, reducing latency and connection overhead.
export default {
async fetch(request: Request, env: Env) {
// Connect to external Postgres via Hyperdrive
const client = new Client({
connectionString: env.HYPERDRIVE_URL
})
await client.connect()
try {
const result = await client.query(
'SELECT * FROM users WHERE id = $1',
[request.params.id]
)
return Response.json(result.rows[0])
} finally {
await client.end()
}
}
}
Turnstile provides bot protection without the user friction of traditional CAPTCHAs.
export async function verifyTurnstile(
token: string,
secret: string
): Promise<boolean> {
const response = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret,
response: token
})
}
)
const data = await response.json()
return data.success
}
// In your Worker
export default {
async fetch(request: Request, env: Env) {
if (request.method === 'POST') {
const { token, ...formData } = await request.json()
const verified = await verifyTurnstile(token, env.TURNSTILE_SECRET)
if (!verified) {
return new Response('Bot detected', { status: 403 })
}
// Process legitimate request
return handleFormSubmission(formData)
}
}
}
const additionalServiceLimits = {
queues: {
messages: "100K/day free",
messageSize: "128KB max",
retention: "4 days"
},
durableObjects: {
requests: "1M/month free",
storage: "50MB per object",
concurrency: "Single-threaded"
},
emailRouting: {
addresses: "Unlimited",
rules: "200 per zone",
size: "25MB per email"
},
analyticsEngine: {
writes: "100K/day free",
retention: "90 days",
queries: "Unlimited"
},
browserRendering: {
included: "1000/month free",
timeout: "60 seconds",
concurrency: "2 browsers"
},
turnstile: {
verifications: "1M/month free",
widgets: "Unlimited"
}
}
const serviceSelector = {
// Real-time features
"websockets": "Durable Objects",
"collaboration": "Durable Objects",
"gaming": "Durable Objects",
// Background processing
"emailProcessing": "Queues + Email Routing",
"webhooks": "Queues",
"batchJobs": "Queues + Cron Triggers",
// Analytics
"userTracking": "Analytics Engine",
"customMetrics": "Analytics Engine",
"privacyFirst": "Analytics Engine (no cookies)",
// Document generation
"invoices": "Browser Rendering",
"reports": "Browser Rendering + R2",
"screenshots": "Browser Rendering",
// External data
"postgres": "Hyperdrive",
"mysql": "Hyperdrive",
"mongodb": "Direct fetch (no pooling)"
}
// Combining multiple services for a SaaS application
export class SaaSPlatform {
async handleRequest(request: Request, env: Env) {
const url = new URL(request.url)
// Protect forms with Turnstile
if (url.pathname === '/api/register') {
const { token, ...data } = await request.json()
if (!await verifyTurnstile(token, env.TURNSTILE_SECRET)) {
return new Response('Invalid captcha', { status: 403 })
}
// Create user in D1
const user = await this.createUser(data, env)
// Queue welcome email
await env.EMAIL_QUEUE.send({
type: 'welcome',
userId: user.id,
email: user.email
})
// Track analytics
await env.ANALYTICS.writeDataPoint({
dataset: 'signups',
point: {
source: request.headers.get('referer'),
timestamp: Date.now()
}
})
return Response.json({ success: true })
}
// WebSocket for real-time features
if (url.pathname.startsWith('/ws/')) {
const roomId = url.pathname.split('/')[2]
const id = env.ROOMS.idFromName(roomId)
const room = env.ROOMS.get(id)
return room.fetch(request)
}
// Generate reports
if (url.pathname === '/api/report') {
const report = await this.generateReport(env)
// Queue PDF generation
await env.QUEUE.send({
type: 'generate_pdf',
reportId: report.id,
userId: request.userId
})
return Response.json({ reportId: report.id })
}
}
}
These additional services enable sophisticated applications that would typically require multiple vendors and significant costs. Each service solves specific needs:
- Queues: Reliable async processing and background jobs
- Durable Objects: Stateful computing for real-time features and coordination
- Email Routing: Programmatic email handling and automation
- Analytics Engine: Privacy-first custom analytics
- Browser Rendering: Screenshot and PDF generation
- Turnstile: Bot protection without user friction
Understanding when to use each service is key to building complete production applications on Cloudflare's platform.
Next: Integration Cookbook - Connecting third-party services
Now that we've explored Cloudflare's comprehensive service offerings, let's address a crucial aspect of real-world applications: integrating with third-party services. While Cloudflare provides the infrastructure foundation, most applications need authentication, payment processing, email delivery, and analytics. This cookbook demonstrates how to connect these essential services while maximizing the value of your $0 infrastructure.
const integrationStrategy = {
auth: "Stateless JWT validation at edge",
payments: "Webhook processing with Queues",
email: "Transactional only, batch with Queues",
analytics: "Edge collection, batch upload",
cdn: "Cache external API responses in KV"
}
Clerk offers a generous free tier (5,000 monthly active users) and edge-friendly architecture.
// workers/auth-middleware.ts
import { Hono } from 'hono'
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://your-app.clerk.accounts.dev/.well-known/jwks.json')
)
export async function clerkAuth(c: any, next: any) {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'No token provided' }, 401)
}
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://your-app.clerk.accounts.dev',
})
// Cache user data in KV for faster subsequent requests
const userKey = `user:${payload.sub}`
let userData = await c.env.KV.get(userKey, 'json')
if (!userData) {
// Fetch full user data from Clerk
userData = await fetchClerkUser(payload.sub, c.env.CLERK_SECRET)
// Cache for 5 minutes
await c.env.KV.put(userKey, JSON.stringify(userData), {
expirationTtl: 300
})
}
c.set('user', userData)
await next()
} catch (error) {
return c.json({ error: 'Invalid token' }, 401)
}
}
// Webhook handler for Clerk events
export async function handleClerkWebhook(c: any) {
const svix_id = c.req.header('svix-id')
const svix_timestamp = c.req.header('svix-timestamp')
const svix_signature = c.req.header('svix-signature')
const body = await c.req.text()
// Verify webhook signature
const wh = new Webhook(c.env.CLERK_WEBHOOK_SECRET)
const evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
})
// Queue event for processing
await c.env.AUTH_QUEUE.send({
type: evt.type,
data: evt.data,
timestamp: Date.now()
})
return c.text('OK')
}
// Queue consumer for auth events
export async function processAuthEvents(batch: MessageBatch, env: Env) {
for (const message of batch.messages) {
const { type, data } = message.body
switch (type) {
case 'user.created':
await env.DB.prepare(`
INSERT INTO users (id, email, name, created_at)
VALUES (?, ?, ?, ?)
`).bind(
data.id,
data.email_addresses[0].email_address,
`${data.first_name} ${data.last_name}`,
new Date(data.created_at).toISOString()
).run()
break
case 'user.deleted':
// Soft delete to maintain referential integrity
await env.DB.prepare(`
UPDATE users SET deleted_at = ? WHERE id = ?
`).bind(new Date().toISOString(), data.id).run()
break
}
message.ack()
}
}
Auth0's free tier includes 7,000 active users and works well with edge computing.
// Edge-optimized Auth0 integration
export class Auth0Integration {
private jwksClient: any
constructor(private domain: string, private audience: string) {
this.jwksClient = createRemoteJWKSet(
new URL(`https://${domain}/.well-known/jwks.json`)
)
}
async verifyToken(token: string): Promise<any> {
try {
const { payload } = await jwtVerify(token, this.jwksClient, {
issuer: `https://${this.domain}/`,
audience: this.audience,
})
return payload
} catch (error) {
throw new Error('Invalid token')
}
}
// Management API calls with caching
async getUser(userId: string, env: Env): Promise<any> {
const cacheKey = `auth0:user:${userId}`
const cached = await env.KV.get(cacheKey, 'json')
if (cached) return cached
const mgmtToken = await this.getManagementToken(env)
const response = await fetch(
`https://${this.domain}/api/v2/users/${userId}`,
{
headers: {
Authorization: `Bearer ${mgmtToken}`,
}
}
)
const user = await response.json()
// Cache user data
await env.KV.put(cacheKey, JSON.stringify(user), {
expirationTtl: 3600 // 1 hour
})
return user
}
private async getManagementToken(env: Env): Promise<string> {
// Cache management token
const cached = await env.KV.get('auth0:mgmt:token')
if (cached) return cached
const response = await fetch(`https://${this.domain}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: env.AUTH0_CLIENT_ID,
client_secret: env.AUTH0_CLIENT_SECRET,
audience: `https://${this.domain}/api/v2/`,
grant_type: 'client_credentials',
})
})
const { access_token, expires_in } = await response.json()
// Cache with expiration
await env.KV.put('auth0:mgmt:token', access_token, {
expirationTtl: expires_in - 300 // 5 min buffer
})
return access_token
}
}
Supabase offers generous free tier with 50,000 monthly active users.
// Supabase edge integration
export class SupabaseAuth {
constructor(
private supabaseUrl: string,
private supabaseAnonKey: string
) {}
async verifyToken(token: string, env: Env): Promise<any> {
// Verify JWT with Supabase's public key
const JWKS = createRemoteJWKSet(
new URL(`${this.supabaseUrl}/auth/v1/jwks`)
)
const { payload } = await jwtVerify(token, JWKS)
// Sync user to D1 if needed
await this.syncUser(payload, env)
return payload
}
private async syncUser(payload: any, env: Env) {
const existing = await env.DB
.prepare('SELECT id FROM users WHERE id = ?')
.bind(payload.sub)
.first()
if (!existing) {
await env.DB.prepare(`
INSERT INTO users (id, email, metadata, created_at)
VALUES (?, ?, ?, ?)
`).bind(
payload.sub,
payload.email,
JSON.stringify(payload.user_metadata || {}),
new Date().toISOString()
).run()
}
}
// Handle Supabase webhooks
async handleWebhook(request: Request, env: Env): Promise<Response> {
const signature = request.headers.get('webhook-signature')
const body = await request.text()
// Verify webhook
if (!this.verifyWebhookSignature(body, signature, env.SUPABASE_WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 })
}
const event = JSON.parse(body)
// Queue for processing
await env.AUTH_QUEUE.send(event)
return new Response('OK')
}
private verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
// Implementation depends on Supabase's webhook signing method
return true
}
}
Stripe's webhook-based architecture works perfectly with Workers and Queues.
// Stripe webhook handler with signature verification
import Stripe from 'stripe'
export class StripeIntegration {
private stripe: Stripe
constructor(apiKey: string) {
this.stripe = new Stripe(apiKey, {
apiVersion: '2023-10-16',
httpClient: Stripe.createFetchHttpClient(), // Use fetch for Workers
})
}
async handleWebhook(request: Request, env: Env): Promise<Response> {
const signature = request.headers.get('stripe-signature')!
const body = await request.text()
let event: Stripe.Event
try {
event = this.stripe.webhooks.constructEvent(
body,
signature,
env.STRIPE_WEBHOOK_SECRET
)
} catch (err) {
return new Response('Invalid signature', { status: 400 })
}
// Queue event for processing
await env.PAYMENT_QUEUE.send({
id: event.id,
type: event.type,
data: event.data,
created: event.created
})
// Immediate response to Stripe
return new Response('OK')
}
// Payment intent creation with idempotency
async createPaymentIntent(params: {
amount: number
currency: string
customerId: string
metadata?: Record<string, string>
}, env: Env): Promise<any> {
const idempotencyKey = `pi_${params.customerId}_${Date.now()}`
// Check if already created
const existing = await env.KV.get(`stripe:pi:${idempotencyKey}`)
if (existing) return JSON.parse(existing)
const paymentIntent = await this.stripe.paymentIntents.create({
amount: params.amount,
currency: params.currency,
customer: params.customerId,
metadata: params.metadata,
}, {
idempotencyKey
})
// Cache the payment intent
await env.KV.put(
`stripe:pi:${idempotencyKey}`,
JSON.stringify(paymentIntent),
{ expirationTtl: 3600 }
)
return paymentIntent
}
}
// Queue processor for Stripe events
export async function processStripeEvents(batch: MessageBatch, env: Env) {
const stripe = new StripeIntegration(env.STRIPE_API_KEY)
for (const message of batch.messages) {
const event = message.body
try {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object, env)
break
case 'customer.subscription.created':
case 'customer.subscription.updated':
await syncSubscription(event.data.object, env)
break
case 'customer.subscription.deleted':
await handleSubscriptionCancellation(event.data.object, env)
break
case 'invoice.payment_failed':
await handleFailedPayment(event.data.object, env)
break
}
message.ack()
} catch (error) {
// Retry logic
if (message.attempts < 3) {
message.retry({ delaySeconds: Math.pow(2, message.attempts) * 60 })
} else {
// Send to dead letter queue
await env.DEAD_LETTER_QUEUE.send({
event,
error: error.message,
attempts: message.attempts
})
message.ack()
}
}
}
}
async function handlePaymentSuccess(paymentIntent: any, env: Env) {
// Update order status
await env.DB.prepare(`
UPDATE orders
SET status = 'paid', paid_at = ?, payment_intent_id = ?
WHERE id = ?
`).bind(
new Date().toISOString(),
paymentIntent.id,
paymentIntent.metadata.order_id
).run()
// Queue order fulfillment
await env.FULFILLMENT_QUEUE.send({
orderId: paymentIntent.metadata.order_id,
customerId: paymentIntent.customer
})
}
async function syncSubscription(subscription: any, env: Env) {
await env.DB.prepare(`
INSERT INTO subscriptions (
id, customer_id, status, current_period_end, plan_id
) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
status = excluded.status,
current_period_end = excluded.current_period_end,
plan_id = excluded.plan_id
`).bind(
subscription.id,
subscription.customer,
subscription.status,
new Date(subscription.current_period_end * 1000).toISOString(),
subscription.items.data[0].price.id
).run()
}
Paddle handles tax compliance and acts as merchant of record.
// Paddle webhook handler
export class PaddleIntegration {
async handleWebhook(request: Request, env: Env): Promise<Response> {
const body = await request.formData()
const signature = body.get('p_signature')
// Verify webhook
if (!this.verifySignature(body, signature, env.PADDLE_PUBLIC_KEY)) {
return new Response('Invalid signature', { status: 401 })
}
const eventType = body.get('alert_name')
// Queue for processing
await env.PAYMENT_QUEUE.send({
type: eventType,
data: Object.fromEntries(body.entries()),
timestamp: Date.now()
})
return new Response('OK')
}
private verifySignature(
body: FormData,
signature: string,
publicKey: string
): boolean {
// Paddle signature verification
const data = new URLSearchParams()
for (const [key, value] of body.entries()) {
if (key !== 'p_signature') {
data.append(key, value.toString())
}
}
// Verify with public key (implementation depends on crypto library)
return true
}
// Subscription management
async updateSubscription(
subscriptionId: string,
updates: any,
env: Env
): Promise<void> {
const response = await fetch(
'https://vendors.paddle.com/api/2.0/subscription/users/update',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
vendor_id: env.PADDLE_VENDOR_ID,
vendor_auth_code: env.PADDLE_AUTH_CODE,
subscription_id: subscriptionId,
...updates
})
}
)
if (!response.ok) {
throw new Error('Failed to update subscription')
}
}
}
Resend offers 3,000 free emails/month with great developer experience.
// Email service with queue batching
export class EmailService {
constructor(private resendApiKey: string) {}
// Queue email for sending
async queueEmail(env: Env, email: {
to: string | string[]
subject: string
template: string
data: Record<string, any>
tags?: string[]
}) {
await env.EMAIL_QUEUE.send({
provider: 'resend',
email,
timestamp: Date.now()
})
}
// Batch processor for email queue
async processEmailBatch(batch: MessageBatch, env: Env) {
const emails = batch.messages.map(m => m.body.email)
// Group by template for batch sending
const grouped = this.groupByTemplate(emails)
for (const [template, group] of grouped) {
try {
await this.sendBatch(group, env)
// Ack all messages in group
group.forEach(({ message }) => message.ack())
} catch (error) {
// Retry individual emails
group.forEach(({ message, email }) => {
message.retry({ delaySeconds: 300 })
})
}
}
}
private async sendBatch(emails: any[], env: Env) {
const response = await fetch('https://api.resend.com/emails/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.resendApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(
emails.map(({ email }) => ({
from: env.FROM_EMAIL,
to: email.to,
subject: email.subject,
html: await this.renderTemplate(email.template, email.data),
tags: email.tags,
}))
)
})
if (!response.ok) {
throw new Error(`Email batch failed: ${response.statusText}`)
}
// Log sent emails
const results = await response.json()
await this.logEmailsSent(results.data, env)
}
private async renderTemplate(
template: string,
data: Record<string, any>
): Promise<string> {
// Simple template rendering - in production use a proper engine
let html = templates[template] || ''
for (const [key, value] of Object.entries(data)) {
html = html.replace(new RegExp(`{{${key}}}`, 'g'), value)
}
return html
}
private async logEmailsSent(emails: any[], env: Env) {
const logs = emails.map(email => ({
id: email.id,
to: email.to,
subject: email.subject,
sent_at: new Date().toISOString()
}))
// Batch insert to D1
await env.DB.batch(
logs.map(log =>
env.DB.prepare(`
INSERT INTO email_logs (id, to_email, subject, sent_at)
VALUES (?, ?, ?, ?)
`).bind(log.id, log.to, log.subject, log.sent_at)
)
)
}
}
// Email templates
const templates = {
welcome: `
<h1>Welcome {{name}}!</h1>
<p>Thanks for joining our platform.</p>
`,
order_confirmation: `
<h1>Order Confirmed</h1>
<p>Order #{{orderId}} has been confirmed.</p>
<p>Total: {{total}}</p>
`,
password_reset: `
<h1>Reset Your Password</h1>
<p>Click <a href="{{resetLink}}">here</a> to reset your password.</p>
`
}
SendGrid offers 100 emails/day free forever.
// SendGrid with template caching
export class SendGridService {
private templates = new Map<string, any>()
constructor(private apiKey: string) {}
async sendEmail(params: {
to: string
templateId: string
dynamicData: Record<string, any>
}, env: Env): Promise<void> {
// Get cached template
let template = this.templates.get(params.templateId)
if (!template) {
template = await this.getTemplate(params.templateId)
this.templates.set(params.templateId, template)
}
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{
to: [{ email: params.to }],
dynamic_template_data: params.dynamicData,
}],
from: { email: env.FROM_EMAIL },
template_id: params.templateId,
})
})
if (!response.ok) {
throw new Error(`SendGrid error: ${response.statusText}`)
}
}
private async getTemplate(templateId: string): Promise<any> {
const response = await fetch(
`https://api.sendgrid.com/v3/templates/${templateId}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
}
}
)
return response.json()
}
}
Twilio's pay-as-you-go model works well with event-driven architectures.
// Twilio SMS with rate limiting
export class TwilioService {
constructor(
private accountSid: string,
private authToken: string,
private fromNumber: string
) {}
async sendSMS(to: string, body: string, env: Env): Promise<void> {
// Rate limit check
const rateLimitKey = `sms:rate:${to}`
const count = parseInt(await env.KV.get(rateLimitKey) || '0')
if (count >= 5) { // 5 SMS per hour per number
throw new Error('SMS rate limit exceeded')
}
const response = await fetch(
`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`,
{
method: 'POST',
headers: {
'Authorization': 'Basic ' + btoa(`${this.accountSid}:${this.authToken}`),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
To: to,
From: this.fromNumber,
Body: body,
})
}
)
if (!response.ok) {
throw new Error(`Twilio error: ${response.statusText}`)
}
// Update rate limit
await env.KV.put(rateLimitKey, (count + 1).toString(), {
expirationTtl: 3600 // 1 hour
})
// Log SMS
const result = await response.json()
await env.DB.prepare(`
INSERT INTO sms_logs (sid, to_number, body, status, sent_at)
VALUES (?, ?, ?, ?, ?)
`).bind(
result.sid,
to,
body,
result.status,
new Date().toISOString()
).run()
}
// Handle status callbacks
async handleStatusCallback(request: Request, env: Env): Promise<Response> {
const formData = await request.formData()
const sid = formData.get('MessageSid')
const status = formData.get('MessageStatus')
await env.DB.prepare(`
UPDATE sms_logs SET status = ? WHERE sid = ?
`).bind(status, sid).run()
return new Response('OK')
}
}
Mixpanel offers 100K monthly tracked users free.
// Edge-optimized Mixpanel tracking
export class MixpanelService {
private queue: any[] = []
private flushTimer: number | null = null
constructor(private projectToken: string) {}
track(event: string, properties: Record<string, any>, env: Env) {
this.queue.push({
event,
properties: {
...properties,
time: Date.now(),
distinct_id: properties.userId || 'anonymous',
$insert_id: crypto.randomUUID(),
}
})
// Auto-flush after 10 events or 5 seconds
if (this.queue.length >= 10) {
this.flush(env)
} else if (!this.flushTimer) {
this.flushTimer = setTimeout(() => this.flush(env), 5000)
}
}
async flush(env: Env) {
if (this.queue.length === 0) return
const events = [...this.queue]
this.queue = []
if (this.flushTimer) {
clearTimeout(this.flushTimer)
this.flushTimer = null
}
// Batch send to Mixpanel
const response = await fetch('https://api.mixpanel.com/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(events.map(e => ({
event: e.event,
properties: {
...e.properties,
token: this.projectToken,
}
})))
})
if (!response.ok) {
// Queue failed events for retry
await env.ANALYTICS_QUEUE.send({
provider: 'mixpanel',
events,
attempts: 1
})
}
}
// User profile updates
async updateUserProfile(
userId: string,
properties: Record<string, any>
): Promise<void> {
const response = await fetch('https://api.mixpanel.com/engage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
$token: this.projectToken,
$distinct_id: userId,
$set: properties,
})
})
if (!response.ok) {
throw new Error('Failed to update user profile')
}
}
}
// Analytics middleware
export function analyticsMiddleware(mixpanel: MixpanelService) {
return async (c: any, next: any) => {
const start = Date.now()
await next()
// Track API request
mixpanel.track('api_request', {
path: c.req.path,
method: c.req.method,
status: c.res.status,
duration: Date.now() - start,
userId: c.get('user')?.id,
ip: c.req.header('CF-Connecting-IP'),
country: c.req.header('CF-IPCountry'),
}, c.env)
}
}
PostHog offers 1M events/month free with self-serve analytics.
// PostHog edge integration
export class PostHogService {
constructor(
private apiKey: string,
private host: string = 'https://app.posthog.com'
) {}
async capture(params: {
distinctId: string
event: string
properties?: Record<string, any>
timestamp?: Date
}, env: Env): Promise<void> {
// Batch events in KV
const batchKey = `posthog:batch:${Date.now()}`
const batch = await env.KV.get(batchKey, 'json') || []
batch.push({
distinct_id: params.distinctId,
event: params.event,
properties: {
...params.properties,
$lib: 'cloudflare-workers',
$lib_version: '1.0.0',
},
timestamp: params.timestamp || new Date(),
})
await env.KV.put(batchKey, JSON.stringify(batch), {
expirationTtl: 60 // 1 minute
})
// Flush if batch is large enough
if (batch.length >= 20) {
await this.flushBatch(batch, env)
await env.KV.delete(batchKey)
}
}
private async flushBatch(batch: any[], env: Env): Promise<void> {
const response = await fetch(`${this.host}/batch/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: this.apiKey,
batch,
})
})
if (!response.ok) {
// Queue for retry
await env.ANALYTICS_QUEUE.send({
provider: 'posthog',
batch,
attempts: 1
})
}
}
// Feature flags with caching
async getFeatureFlags(distinctId: string, env: Env): Promise<Record<string, boolean>> {
const cacheKey = `posthog:flags:${distinctId}`
const cached = await env.KV.get(cacheKey, 'json')
if (cached) return cached
const response = await fetch(`${this.host}/decide/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: this.apiKey,
distinct_id: distinctId,
})
})
const data = await response.json()
const flags = data.featureFlags || {}
// Cache for 5 minutes
await env.KV.put(cacheKey, JSON.stringify(flags), {
expirationTtl: 300
})
return flags
}
}
Cloudinary offers 25GB storage and 25GB bandwidth free.
// Cloudinary with R2 backup
export class CloudinaryService {
constructor(
private cloudName: string,
private apiKey: string,
private apiSecret: string
) {}
async uploadImage(
file: File,
options: {
folder?: string
transformation?: string
backup?: boolean
},
env: Env
): Promise<{
url: string
publicId: string
format: string
}> {
// Generate signature
const timestamp = Math.round(Date.now() / 1000)
const params = {
timestamp,
folder: options.folder,
transformation: options.transformation,
}
const signature = this.generateSignature(params)
// Upload to Cloudinary
const formData = new FormData()
formData.append('file', file)
formData.append('api_key', this.apiKey)
formData.append('timestamp', timestamp.toString())
formData.append('signature', signature)
if (options.folder) {
formData.append('folder', options.folder)
}
const response = await fetch(
`https://api.cloudinary.com/v1_1/${this.cloudName}/image/upload`,
{
method: 'POST',
body: formData,
}
)
const result = await response.json()
// Backup to R2 if requested
if (options.backup) {
await env.BUCKET.put(
`cloudinary-backup/${result.public_id}.${result.format}`,
file,
{
customMetadata: {
cloudinaryUrl: result.secure_url,
uploadedAt: new Date().toISOString(),
}
}
)
}
return {
url: result.secure_url,
publicId: result.public_id,
format: result.format,
}
}
private generateSignature(params: Record<string, any>): string {
const sortedParams = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&')
return crypto
.createHash('sha256')
.update(sortedParams + this.apiSecret)
.digest('hex')
}
// Generate transformation URLs
getTransformedUrl(
publicId: string,
transformation: string
): string {
return `https://res.cloudinary.com/${this.cloudName}/image/upload/${transformation}/${publicId}`
}
}
// Unified retry logic for all integrations
export class IntegrationRetryHandler {
async executeWithRetry<T>(
operation: () => Promise<T>,
options: {
maxAttempts?: number
backoff?: 'exponential' | 'linear'
initialDelay?: number
maxDelay?: number
onError?: (error: any, attempt: number) => void
} = {}
): Promise<T> {
const maxAttempts = options.maxAttempts || 3
const backoff = options.backoff || 'exponential'
const initialDelay = options.initialDelay || 1000
const maxDelay = options.maxDelay || 30000
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation()
} catch (error) {
if (options.onError) {
options.onError(error, attempt)
}
if (attempt === maxAttempts) {
throw error
}
const delay = backoff === 'exponential'
? Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay)
: initialDelay * attempt
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw new Error('Max attempts exceeded')
}
}
// Usage example
const retryHandler = new IntegrationRetryHandler()
const result = await retryHandler.executeWithRetry(
async () => {
return await stripe.paymentIntents.create(params)
},
{
maxAttempts: 3,
backoff: 'exponential',
onError: (error, attempt) => {
console.error(`Stripe error (attempt ${attempt}):`, error)
}
}
)
// Unified webhook verification
export class WebhookVerifier {
private verifiers = new Map<string, (body: string, signature: string) => boolean>()
constructor(secrets: Record<string, string>) {
// Register verifiers for each service
this.verifiers.set('stripe', (body, signature) => {
// Stripe verification logic
return true
})
this.verifiers.set('clerk', (body, signature) => {
// Clerk verification logic
return true
})
// Add more verifiers as needed
}
verify(
service: string,
body: string,
signature: string
): boolean {
const verifier = this.verifiers.get(service)
if (!verifier) {
throw new Error(`Unknown service: ${service}`)
}
return verifier(body, signature)
}
}
// Track integration usage to stay within free tiers
export class IntegrationUsageTracker {
async track(
service: string,
operation: string,
count: number = 1,
env: Env
): Promise<void> {
const today = new Date().toISOString().split('T')[0]
const key = `usage:${service}:${today}`
const current = await env.KV.get(key, 'json') || {}
current[operation] = (current[operation] || 0) + count
await env.KV.put(key, JSON.stringify(current), {
expirationTtl: 86400 * 7 // Keep for 7 days
})
// Check limits
await this.checkLimits(service, current, env)
}
private async checkLimits(
service: string,
usage: Record<string, number>,
env: Env
): Promise<void> {
const limits = {
resend: { emails: 3000 },
clerk: { activeUsers: 5000 },
mixpanel: { events: 100000 },
// Add more limits
}
const serviceLimits = limits[service]
if (!serviceLimits) return
for (const [operation, limit] of Object.entries(serviceLimits)) {
if (usage[operation] > limit * 0.8) { // 80% threshold
await this.sendLimitWarning(service, operation, usage[operation], limit, env)
}
}
}
private async sendLimitWarning(
service: string,
operation: string,
usage: number,
limit: number,
env: Env
): Promise<void> {
// Queue warning email
await env.EMAIL_QUEUE.send({
to: env.ADMIN_EMAIL,
subject: `${service} usage warning`,
template: 'limit_warning',
data: {
service,
operation,
usage,
limit,
percentage: Math.round((usage / limit) * 100)
}
})
}
}
We've explored an entire ecosystem of services that, combined, create a production-ready infrastructure stack that costs absolutely nothing to start and scales globally by default. This isn't about building toy projects—it's about fundamentally rethinking how we approach web infrastructure.
Throughout this guide, we've assembled a complete platform:
const zeroInfrastructureStack = {
// Frontend Delivery
hosting: "Cloudflare Pages - Unlimited bandwidth",
cdn: "Global network - 300+ locations",
// Compute Layer
api: "Workers - 100K requests/day",
framework: "Hono - 12KB, zero dependencies",
// Data Storage
database: "D1 - 5GB SQLite at edge",
keyValue: "KV - 1GB distributed storage",
files: "R2 - 10GB with zero egress",
vectors: "Vectorize - 5M embeddings",
// Intelligence
ai: "Workers AI - 10K daily inferences",
search: "Semantic search via Vectorize",
// Additional Services
queues: "100K messages/day",
realtime: "Durable Objects for WebSockets",
email: "Email routing and processing",
analytics: "Privacy-first metrics"
}
When you combine these services, a powerful architecture naturally emerges:
┌─────────────────────────────────────────────────────────┐
│ Global Edge Network │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Pages │───▶│ Workers │───▶│ D1 (SQL) │ │
│ │ (React) │ │ (Hono API) │ └────────────┘ │
│ └─────────────┘ └──────┬───────┘ │
│ │ │
│ ┌─────┴─────┬──────┬──────┐ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌────┐ ┌──────┐ │
│ │ KV │ │ R2 │ │Vec │ │ AI │ │
│ └─────┘ └─────┘ └────┘ └──────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Queues │ │ Durable │ │ Email │ │
│ │ (Async) │ │ Objects │ │ Routing │ │
│ └─────────────┘ └──────────────┘ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
This isn't theoretical. Here are production applications running on this stack:
const saasArchitecture = {
frontend: "Pages - React dashboard",
api: "Workers + Hono - REST/GraphQL",
auth: "Workers + JWT + KV sessions",
database: "D1 - User data, subscriptions",
files: "R2 - User uploads, exports",
async: "Queues - Email, webhooks, reports",
search: "Vectorize - Semantic document search",
ai: "Workers AI - Smart features"
}
// Supports 5,000+ daily active users on free tier
const ecommerceStack = {
storefront: "Pages - Static product pages",
cart: "Workers + KV - Session management",
inventory: "D1 - Product catalog",
images: "R2 - Product photos (zero egress!)",
search: "Vectorize - Product discovery",
ai: "Workers AI - Recommendations",
email: "Email Routing - Order confirmations",
analytics: "Analytics Engine - Conversion tracking"
}
// Handles 100K+ daily visitors free
const contentPlatform = {
site: "Pages - Blog/documentation",
api: "Workers - Content API",
content: "D1 + R2 - Articles and media",
search: "Vectorize - Semantic search",
ai: "Workers AI - Auto-tagging, summaries",
realtime: "Durable Objects - Live comments",
moderation: "Workers AI - Content filtering"
}
// Serves millions of pageviews free
Let's talk real numbers. Here's what this same infrastructure would cost elsewhere:
// Monthly costs for a typical SaaS (100K daily requests, 50GB bandwidth, 10GB storage)
const traditionalCosts = {
aws: {
ec2: 50, // t3.small instance
rds: 15, // db.t3.micro
s3: 5, // Storage
cloudfront: 45, // Bandwidth
total: 115 // Per month
},
vercel: {
hosting: 20, // Pro plan
database: 20, // Postgres
bandwidth: 75, // Bandwidth overages
total: 115 // Per month
},
cloudflare: {
everything: 0 // Seriously, $0
}
}
// Annual savings: $1,380
// That's real money for indie developers and startups
The free tier is generous enough for many production applications. But when you do need to scale:
const scalingPath = {
workers: {
free: "100K requests/day",
paid: "$5/month for 10M requests"
},
kv: {
free: "100K reads/day",
paid: "$0.50/million reads"
},
d1: {
free: "5GB storage, 5M reads/day",
paid: "$0.75/GB storage"
},
r2: {
free: "10GB storage, unlimited egress",
paid: "$0.015/GB storage, still free egress!"
}
}
// Even at scale, dramatically cheaper than alternatives
Through building on this stack, key patterns emerge:
// Every read from KV is free quota
const cacheStrategy = {
browser: "Cache-Control headers",
edge: "Cache API in Workers",
application: "KV for computed results",
database: "Minimize D1 reads"
}
// Design for distributed systems
const edgePatterns = {
consistency: "Embrace eventual consistency",
data: "Denormalize for read performance",
compute: "Move logic to data location",
state: "Use Durable Objects sparingly"
}
// Track your free tier consumption
const monitoring = {
workers: "Built-in analytics",
custom: "Analytics Engine for metrics",
alerts: "Set up usage notifications",
optimization: "Continuously improve"
}
Ready to build your $0 infrastructure? Here's your roadmap:
# 1. Set up Cloudflare account
# 2. Deploy first Pages site
npm create cloudflare@latest my-app
wrangler pages deploy dist
# 3. Add Workers API
npm install hono
# Create API routes
# 4. Connect D1 database
wrangler d1 create my-database
# 1. Add authentication with KV sessions
# 2. Implement file uploads with R2
# 3. Set up Queues for background jobs
# 4. Add Analytics Engine
# 1. Integrate Workers AI for smart features
# 2. Implement Vectorize for search
# 3. Add content moderation
# 4. Build recommendation engine
# 1. Set up monitoring and alerts
# 2. Implement error tracking
# 3. Add automated testing
# 4. Configure custom domains
You're not alone on this journey:
- Discord: Cloudflare Developers community
- GitHub: Example repositories and templates
- Documentation: Comprehensive guides at developers.cloudflare.com
- Templates: Ready-to-deploy starters
We're witnessing a fundamental shift in how applications are built and deployed. The old model of expensive, complex infrastructure is giving way to something better: infrastructure that's free to start, simple to use, and scales infinitely. This isn't just about saving money—though saving thousands annually is nice. It's about democratizing access to production-grade infrastructure.
Edge computing isn't a future technology—it's here now, it's production-ready, and it's free. While others debate serverless cold starts and wrestle with Kubernetes, you can build globally distributed applications that just work.
Here's my challenge to you: Build something. Today.
The barrier to entry has never been lower. The tools have never been better. The opportunity has never been greater. In a few years, we'll look back at the era of regional deployments and egress fees the way we now look at physical servers and data centers—as relics of a more primitive time. You have the opportunity to be ahead of that curve.
Your immediate next steps:
- Pick a Project: Start with something real you want to build
- Deploy Today: Get your first Pages + Workers app live with
npm create cloudflare@latest
- Iterate Quickly: The free tier lets you experiment without fear
- Share Your Journey: Help others discover this new paradigm
The $0 infrastructure stack isn't just about free services—it's about a fundamental reimagining of how we build for the web. It's about infrastructure that empowers rather than constrains, that scales rather than limits, that includes rather than excludes.
Every request served makes the network stronger. Every application built proves the model. Every developer who discovers this approach helps push the industry forward.
Welcome to the edge. Welcome to the future. Welcome to your $0 infrastructure stack.
Now go build something amazing.
Remember: The best time to plant a tree was 20 years ago. The second best time is now. The same applies to building on the edge.
Start here: developers.cloudflare.com
Deploy now: npm create cloudflare@latest
Join us: discord.gg/cloudflaredev