Skip to content

Instantly share code, notes, and snippets.

@garyblankenship
Created July 19, 2025 00:38
Show Gist options
  • Save garyblankenship/27a4c57eca4aa5d659ee3c509668b66d to your computer and use it in GitHub Desktop.
Save garyblankenship/27a4c57eca4aa5d659ee3c509668b66d to your computer and use it in GitHub Desktop.
The $0 Infrastructure Stack

The Complete Cloudflare Free Tier Guide

Generated on: 7/18/2025, 8:22:50 PM


The $0 Infrastructure Stack: Building Production Apps on Cloudflare's Free Tier

Introduction

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.

Why Cloudflare's Free Tier is Different

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)

Your Journey Through the Stack

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

The Edge-First Revolution

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.

What You'll Learn

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:

  1. Static Sites with Dynamic Features: Start with Pages and add API routes
  2. Full-Stack Applications: Integrate D1 database, KV sessions, and R2 storage
  3. AI-Powered Platforms: Add vector search and LLM capabilities
  4. 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

The Real Free Tier Limits

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.

Who This Guide is For

  • 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

Prerequisites

You'll need:

  • Basic JavaScript/TypeScript knowledge
  • A Cloudflare account (free)
  • Node.js installed locally
  • Curiosity about edge computing

The Philosophy

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.

Let's Begin

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


The Real Trade-offs: When the $0 Stack Isn't the Right Fit

An Honest Assessment

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.

Vendor Lock-in: The Cloudflare Commitment

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

The Edge Paradigm: A Different Way of Thinking

Building on the edge isn't just "serverless in more locations." It requires fundamental shifts in how you architect applications.

Eventual Consistency is the Default

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

No Long-Running Processes

Workers have strict CPU limits (10-50ms). This rules out:

  • Video encoding/transcoding
  • Large file processing
  • Complex ML model training
  • Long-polling connections

Distributed State Complexity

// 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
  }
}

When Traditional Cloud is the Better Choice

Let's be explicit about scenarios where AWS, GCP, or Azure might serve you better:

1. Heavy Computational Workloads

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.

2. Complex Relational Data Requirements

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.

3. Specialized Database Needs

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.

4. Legacy System Integration

Use traditional cloud for:

  • On-premise database connections
  • VPN requirements
  • Legacy protocol support (SOAP, etc.)
  • Windows-based applications

5. Regulatory Compliance

Use traditional cloud for:

  • Specific data residency requirements
  • Industries requiring FedRAMP, HIPAA specifics
  • Government contracts with cloud restrictions

The Learning Curve

Be prepared for these paradigm shifts:

  1. Connection pooling doesn't exist - Each request is isolated
  2. No local file system - Everything must be in object storage or memory
  3. Different debugging - No SSH, limited logging, distributed traces
  4. New patterns - Event-driven, not request-driven architectures

Making an Informed Decision

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 Bottom Line

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.


Cloudflare Pages: Your Zero-Cost Global CDN

The 5-Minute Proof

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

The Foundation of Free

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.

What Makes Pages Special

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

Getting Started

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

Beyond Static Files

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.

Advanced Pages Features

Custom Domains

# Add your domain
wrangler pages domain add example.com

# Automatic SSL, global anycast, DDoS protection included

Preview Deployments

Every git branch gets a unique URL:

  • mainmy-app.pages.dev
  • featurefeature.my-app.pages.dev
  • PR #123 → 123.my-app.pages.dev

Build Configuration

// wrangler.toml
{
  "build": {
    "command": "npm run build",
    "output": "dist"
  },
  "env": {
    "production": {
      "vars": {
        "API_URL": "https://api.example.com"
      }
    }
  }
}

Pages + Workers Integration

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.

Real-World Patterns

SPA with API Backend

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

Static Site with Dynamic Features

// 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')
}

Image Optimization

// 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}`)
}

Performance Optimizations

Smart Caching

// 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
}

Asset Optimization

// build config
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'axios']
        }
      }
    }
  }
}

Monitoring and Analytics

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()
}

Cost Analysis

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
}

When to Use Pages

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)

Pages Best Practices

  1. Optimize Assets: Use modern formats (WebP, AVIF)
  2. Enable Compression: Brotli compression is automatic
  3. Use HTTP/3: Enabled by default for all sites
  4. Implement Caching: Set proper cache headers
  5. Monitor Performance: Use Web Analytics (also free)

Advanced Techniques

Incremental Static Regeneration

// 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
}

A/B Testing

// 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()
}

The Pages Ecosystem

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

Summary

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


Workers: Serverless Computing at the Edge

The 5-Minute Proof

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/day

The 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

The Power of Distributed Compute

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.

Why Workers is Revolutionary

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"
  }
}

Your First Worker

// 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 Architecture

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

Connecting Services

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 })
  }
}

Real-World Patterns

RESTful API

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
}

Authentication Middleware

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 
  })
})

Rate Limiting

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)

Advanced Workers Features

Cron Triggers

// 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))
  }
}

WebSocket Support

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)
  })
}

Service Bindings

// 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)
  }
}

Performance Optimization

Request Coalescing

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)
  }
}

Response Streaming

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

Workers Limits and Optimization

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"
}

Integration Patterns

With Pages

// pages/functions/api/[[path]].ts
export { default } from 'my-worker'

With D1 Database

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)
  }
}

With R2 Storage

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 })
  }
}

Debugging and Monitoring

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 })
    }
  }
}

Cost Comparison

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

When to Use Workers

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)

Summary

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


Hono Framework: The Perfect Workers Companion

The 5-Minute Proof

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 app

The 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

Why Hono + Workers = Magic

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.

What Makes Hono Special

// 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"
  }
}

Getting Started with Hono

# 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

Real-World API Patterns

RESTful API with Validation

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)

Authentication & Authorization

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)

File Upload Handling

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)

WebSocket Support

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)

Advanced Hono Features

Middleware Composition

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
    }
  })
)

Error Handling

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)
  }
}

Request/Response Helpers

// 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: '...' })
})

Testing Hono Applications

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)
  })
})

Performance Optimization

Response Streaming

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'
    }
  })
})

Batch Processing

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
    }))
  })
})

Hono Best Practices

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"
  }
}

Hono vs Other Frameworks

// 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"
}

Summary

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


D1 Database: SQLite at the Edge

The 5-Minute Proof

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

Production SQL Database for $0

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.

Why D1 Changes Everything

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"
  }
}

Getting Started with D1

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);

Using D1 in Workers

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 Architecture

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

Advanced Query Patterns

Transactions

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
}

Batch Operations

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
}

Full-Text Search

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

Real-World Patterns

Multi-Tenant SaaS

// 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 Sourcing

// 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))
  }, {})
}

Time-Series 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()
}

Performance Optimization

Indexing Strategy

-- 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));

Query Optimization

// 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()

Connection Pooling

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

// 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()
      })
    }
  }
}

Backup and Export

// 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()
      }
    }
  })
}

Monitoring and Analytics

// 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()
    }
  })
}

D1 Limits and Considerations

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"
}

D1 vs Traditional Databases

// 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"
  }
}

Summary

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


KV Store: Lightning-Fast Key-Value Storage

The 5-Minute Proof

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

Global State Management at the Edge

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.

Understanding KV's Architecture

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"
  }
}

Getting Started with KV

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 })
  }
}

Real-World Patterns

Session Management

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 })
  }
}

Intelligent Caching

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)
  }
}

Feature Flags

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 })
  }
}

Rate Limiting

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()
      }
    })
  }
}

Configuration Management

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 
    })
  }
}

Advanced KV Patterns

Distributed Locks

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)
    }
  }
}

Event Sourcing with KV

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()
      })
    )
  }
}

Performance Optimization

Batch Operations

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

Cache Warming

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 []
  }
}

KV Limits and Best Practices

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)"
}

KV vs Other Storage Options

// 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"
  }
}

Summary

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


R2 Storage: Object Storage Without Egress Fees

The 5-Minute Proof

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

The Game-Changing Storage Economics

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!

Getting Started with R2

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 })
    }
  }
}

Real-World Patterns

Image Upload and Processing

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 })
  }
}

Secure File Sharing

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

Content Management System

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)}`
  }
}

Backup and Archive System

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

Advanced R2 Features

Multipart Uploads

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

Lifecycle Policies

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)
        }
      }
    }
  }
}

Performance Optimization

Edge Caching

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 })
  }
}

Conditional Requests

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 })
}

Cost Optimization

Storage Tiering

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()
  }
}

R2 Limits and Best Practices

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"
}

R2 vs Other Storage Solutions

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

Summary

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


Vectorize: AI-Powered Vector Search at the Edge

The 5-Minute Proof

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

Semantic Search for the Modern Web

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
  ]
}

Getting Started with Vectorize

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
      }))
    })
  }
}

Real-World Applications

Semantic Document Search

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) + '...'
  }
}

Product Recommendation Engine

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

Multi-Modal Search (Text + Image)

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)
  }
}

Question Answering System

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)))
  }
}

Advanced Vectorize Features

Hybrid Search (Vector + Keyword)

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
    }))
  }
}

Time-Aware Vector Search

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)
  }
}

Performance Optimization

Batch Processing

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))
      }
    }
  }
}

Dimension Reduction

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)
    )
  }
}

Vectorize Limits and Best Practices

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"
}

Summary

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


Workers AI: Edge AI Inference at Scale

The 5-Minute Proof

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

Bringing Intelligence to the Edge

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"
  }
}

Getting Started

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
    })
  }
}

Available Models

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
  ]
}

Real-World Applications

Intelligent Chatbot

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 })
  }
}

Content Moderation System

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()
  }
}

AI-Powered Image Generation

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

Document Intelligence

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()
      }
    }])
  }
}

Advanced AI Patterns

Streaming Responses

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'
        }
      })
    }
  }
}

Model Routing

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'
  }
}

Cost Optimization

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

Performance Monitoring

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

Workers AI Limits and Best Practices

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"
}

Summary

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


Additional Services: Completing the Stack

Beyond the Core Services

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: Reliable Message Processing

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

Real-World Queue Patterns

// 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: Stateful Edge Computing

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: Programmatic Email Handling

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: Custom Analytics Without Cookies

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: Screenshots and PDFs

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: Database Connection Pooling

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: Privacy-First CAPTCHA

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)
    }
  }
}

Service Limits Summary

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"
  }
}

Choosing the Right Service

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)"
}

Integration Example: Complete Application

// 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 })
    }
  }
}

Summary

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


Integration Cookbook: Connecting Third-Party Services

Building Complete Applications with External 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.

Core Integration Principles

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"
}

Authentication Providers

Clerk Integration

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 Integration

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 Auth Integration

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

Payment Processing

Stripe Integration

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 Integration

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')
    }
  }
}

Email Services

Resend Integration

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 Integration

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()
  }
}

SMS Services

Twilio Integration

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')
  }
}

Analytics Services

Mixpanel Integration

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 Integration

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

File Storage & CDN

Cloudinary Integration

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}`
  }
}

Integration Best Practices

Error Handling & Retries

// 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)
    }
  }
)

Webhook Security

// 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)
  }
}

Cost Optimization

// 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)
      }
    })
  }
}

Bringing It All Together: Your $0 Infrastructure Journey

From Zero to Production

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.

What We've Built

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"
}

The Architecture That Emerges

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   │ │
│  └─────────────┘    └──────────────┘    └────────────┘ │
│                                                           │
└─────────────────────────────────────────────────────────┘

Real Applications You Can Build

This isn't theoretical. Here are production applications running on this stack:

1. SaaS Platform

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

2. E-commerce Store

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

3. Content Platform

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

The Economics Revolution

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

Scaling Beyond Free

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

Best Practices We've Learned

Through building on this stack, key patterns emerge:

1. Cache Aggressively

// 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"
}

2. Optimize for Edge

// 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"
}

3. Monitor Usage

// Track your free tier consumption
const monitoring = {
  workers: "Built-in analytics",
  custom: "Analytics Engine for metrics",
  alerts: "Set up usage notifications",
  optimization: "Continuously improve"
}

Getting Started Today

Ready to build your $0 infrastructure? Here's your roadmap:

Week 1: Foundation

# 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

Week 2: Enhancement

# 1. Add authentication with KV sessions
# 2. Implement file uploads with R2
# 3. Set up Queues for background jobs
# 4. Add Analytics Engine

Week 3: Intelligence

# 1. Integrate Workers AI for smart features
# 2. Implement Vectorize for search
# 3. Add content moderation
# 4. Build recommendation engine

Week 4: Production

# 1. Set up monitoring and alerts
# 2. Implement error tracking
# 3. Add automated testing
# 4. Configure custom domains

Community and Resources

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

Your Journey to the Edge Starts Now

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:

  1. Pick a Project: Start with something real you want to build
  2. Deploy Today: Get your first Pages + Workers app live with npm create cloudflare@latest
  3. Iterate Quickly: The free tier lets you experiment without fear
  4. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment