Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide20 min read

Cloudflare

Cloudflare is a comprehensive edge computing platform offering CDN, Workers, Pages, D1, R2, security, and DNS for modern applications.

Cloudflare - Complete guide to edge computing

What is Cloudflare?

Cloudflare is a global edge computing platform that has revolutionized the way we build and deliver web applications. Starting as a CDN and DDoS protection service, Cloudflare has transformed into a full-fledged developer platform with Workers (serverless on the edge), Pages (static hosting and SSR), D1 (SQLite database), R2 (object storage), and many other services.

The key differentiator of Cloudflare is edge computing - your code runs in over 300 locations around the world, close to users, providing ultra-low latency (often below 50ms).

Why Cloudflare?

Key advantages

  1. Global edge - 300+ locations, < 50ms latency globally
  2. Zero cold starts - Workers start instantly
  3. Generous free tier - 100k req/day for Workers, 10GB R2
  4. Integrated ecosystem - Workers + D1 + R2 + KV work together seamlessly
  5. Security - WAF, DDoS protection, Bot Management built-in
  6. Developer Experience - Wrangler CLI, local dev environment

Cloudflare vs AWS/Vercel/Netlify

FeatureCloudflareAWS Lambda@EdgeVercelNetlify
Edge locations300+13 (Lambda@Edge)~20~20
Cold start0ms100-500ms~50ms~100ms
Free tier100k req/dayPaid100k req/mo125k req/mo
DatabaseD1 (SQLite)DynamoDBPostgres (Neon)No native
StorageR2 (no egress)S3 (egress fee)BlobNone
Pricing modelPay-per-requestPay-per-durationBandwidthBandwidth

When to choose Cloudflare?

  • Edge API - Ultra-low latency for global users
  • Static sites - Pages with CDN and cache
  • Full-stack apps - Workers + D1 + R2
  • Security - WAF, rate limiting, bot protection
  • Cost optimization - R2 with no egress fees

Cloudflare Workers

What are Workers?

Workers are serverless functions running on the Cloudflare edge - in over 300 locations worldwide. Unlike traditional Lambda functions, Workers use V8 Isolates instead of containers, which means zero cold starts.

Creating a Workers project

Code
Bash
# Install Wrangler CLI
npm install -g wrangler

# Login
wrangler login

# Create a project
npm create cloudflare@latest my-worker

# Or with a template
npm create cloudflare@latest my-api -- --template hono

Basic Worker

TSsrc/index.ts
TypeScript
// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url)

    // Routing
    if (url.pathname === '/') {
      return new Response('Hello from Cloudflare Workers!', {
        headers: { 'Content-Type': 'text/plain' },
      })
    }

    if (url.pathname === '/api/data') {
      return Response.json({
        message: 'Hello from the edge!',
        location: request.cf?.city || 'Unknown',
        timestamp: new Date().toISOString(),
      })
    }

    if (url.pathname.startsWith('/api/users')) {
      return handleUsers(request, env)
    }

    return new Response('Not Found', { status: 404 })
  },
} satisfies ExportedHandler<Env>

async function handleUsers(request: Request, env: Env): Promise<Response> {
  const url = new URL(request.url)
  const userId = url.pathname.split('/')[3]

  switch (request.method) {
    case 'GET':
      if (userId) {
        // Get single user
        const user = await env.DB.prepare(
          'SELECT * FROM users WHERE id = ?'
        ).bind(userId).first()

        if (!user) {
          return Response.json({ error: 'User not found' }, { status: 404 })
        }
        return Response.json(user)
      }
      // Get all users
      const { results } = await env.DB.prepare('SELECT * FROM users').all()
      return Response.json(results)

    case 'POST':
      const body = await request.json<{ name: string; email: string }>()
      const result = await env.DB.prepare(
        'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
      ).bind(body.name, body.email).first()
      return Response.json(result, { status: 201 })

    default:
      return new Response('Method not allowed', { status: 405 })
  }
}

wrangler.toml configuration

wrangler.toml
TOML
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# Node.js compatibility
compatibility_flags = ["nodejs_compat"]

# Environment variables
[vars]
API_VERSION = "1.0.0"

# Secrets (add via CLI: wrangler secret put API_KEY)
# API_KEY = "..."

# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxx-xxxx-xxxx-xxxx"

# KV Namespace
[[kv_namespaces]]
binding = "KV"
id = "xxxxx-xxxx-xxxx-xxxx"

# R2 Bucket
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"

# Durable Objects
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"

# Cron Triggers
[triggers]
crons = ["0 * * * *"]  # Every hour

# Routes
routes = [
  { pattern = "api.example.com/*", zone_name = "example.com" }
]

# Preview environment
[env.preview]
name = "my-worker-preview"
vars = { ENVIRONMENT = "preview" }

# Production environment
[env.production]
name = "my-worker-production"
vars = { ENVIRONMENT = "production" }
routes = [
  { pattern = "api.example.com/*", zone_name = "example.com" }
]

TypeScript types

TSsrc/types.ts
TypeScript
// src/types.ts
export interface Env {
  // D1 Database
  DB: D1Database

  // KV Namespace
  KV: KVNamespace

  // R2 Bucket
  BUCKET: R2Bucket

  // Durable Object
  COUNTER: DurableObjectNamespace

  // Environment variables
  API_VERSION: string
  API_KEY: string
}

// Request with Cloudflare properties
interface CfProperties {
  city?: string
  country?: string
  continent?: string
  latitude?: string
  longitude?: string
  timezone?: string
  region?: string
  asn?: number
  colo?: string
}

declare global {
  interface Request {
    cf?: CfProperties
  }
}

Hono Framework (recommended)

Code
Bash
npm create cloudflare@latest my-api -- --template hono
TSsrc/index.ts
TypeScript
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'

type Bindings = {
  DB: D1Database
  KV: KVNamespace
  JWT_SECRET: string
}

const app = new Hono<{ Bindings: Bindings }>()

// Middleware
app.use('*', logger())
app.use('/api/*', cors())

// Public routes
app.get('/', (c) => c.text('Hello Hono on Workers!'))

app.get('/api/health', (c) => {
  return c.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    location: c.req.raw.cf?.city || 'Unknown',
  })
})

// Protected routes
app.use('/api/protected/*', jwt({ secret: 'your-secret' }))

app.get('/api/protected/profile', async (c) => {
  const payload = c.get('jwtPayload')
  return c.json({ user: payload })
})

// CRUD for users
app.get('/api/users', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM users ORDER BY created_at DESC'
  ).all()
  return c.json(results)
})

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

app.post('/api/users', async (c) => {
  const body = await c.req.json<{ name: string; email: string }>()

  const result = await c.env.DB.prepare(
    'INSERT INTO users (name, email) VALUES (?, ?) RETURNING *'
  ).bind(body.name, body.email).first()

  return c.json(result, 201)
})

app.put('/api/users/:id', async (c) => {
  const id = c.req.param('id')
  const body = await c.req.json<{ name?: string; email?: string }>()

  const result = await c.env.DB.prepare(
    'UPDATE users SET name = COALESCE(?, name), email = COALESCE(?, email) WHERE id = ? RETURNING *'
  ).bind(body.name, body.email, id).first()

  return c.json(result)
})

app.delete('/api/users/:id', async (c) => {
  const id = c.req.param('id')
  await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run()
  return c.body(null, 204)
})

// Error handling
app.onError((err, c) => {
  console.error(`Error: ${err.message}`)
  return c.json({ error: err.message }, 500)
})

app.notFound((c) => {
  return c.json({ error: 'Not found' }, 404)
})

export default app

Local development

Code
Bash
# Run locally with wrangler
wrangler dev

# With hot reload
wrangler dev --live-reload

# With local bindings (D1, KV, R2)
wrangler dev --local --persist

# On a specific port
wrangler dev --port 8787

Deploy

Code
Bash
# Deploy to Cloudflare
wrangler deploy

# Deploy to a specific environment
wrangler deploy --env production

# Preview deploy
wrangler deploy --env preview

D1 - SQLite on the edge

Creating a D1 database

Code
Bash
# Create a database
wrangler d1 create my-database

# Copy the ID to wrangler.toml
# database_id = "xxxxx-xxxx-xxxx-xxxx"

Schema and migrations

Code
SQL
-- migrations/0001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);

-- migrations/0002_create_posts.sql
CREATE TABLE IF NOT EXISTS posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT,
  author_id INTEGER NOT NULL,
  published BOOLEAN DEFAULT FALSE,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (author_id) REFERENCES users(id)
);

CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_published ON posts(published);
Code
Bash
# Run migrations
wrangler d1 migrations apply my-database

# Locally
wrangler d1 migrations apply my-database --local

Queries in D1

Code
TypeScript
// Basic operations
async function d1Examples(db: D1Database) {
  // SELECT all
  const { results: allUsers } = await db
    .prepare('SELECT * FROM users')
    .all()

  // SELECT with parameters (prepared statement)
  const user = await db
    .prepare('SELECT * FROM users WHERE id = ?')
    .bind(1)
    .first()

  // SELECT with multiple parameters
  const filteredUsers = await db
    .prepare('SELECT * FROM users WHERE name LIKE ? AND created_at > ?')
    .bind('%John%', '2024-01-01')
    .all()

  // INSERT
  const insertResult = await db
    .prepare('INSERT INTO users (name, email) VALUES (?, ?)')
    .bind('John Doe', 'john@example.com')
    .run()

  console.log('Inserted ID:', insertResult.meta.last_row_id)

  // INSERT RETURNING
  const newUser = await db
    .prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
    .bind('Jane Doe', 'jane@example.com')
    .first()

  // UPDATE
  const updateResult = await db
    .prepare('UPDATE users SET name = ? WHERE id = ?')
    .bind('John Smith', 1)
    .run()

  console.log('Rows changed:', updateResult.meta.changes)

  // DELETE
  await db.prepare('DELETE FROM users WHERE id = ?').bind(1).run()

  // Batch operations (transaction)
  const batchResults = await db.batch([
    db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('User 1', 'user1@example.com'),
    db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('User 2', 'user2@example.com'),
    db.prepare('UPDATE posts SET published = TRUE WHERE author_id = ?').bind(1),
  ])

  // Raw SQL (for complex queries)
  const { results } = await db.prepare(`
    SELECT
      u.id,
      u.name,
      COUNT(p.id) as post_count
    FROM users u
    LEFT JOIN posts p ON u.id = p.author_id
    GROUP BY u.id
    ORDER BY post_count DESC
    LIMIT 10
  `).all()

  return results
}

D1 with Drizzle ORM

Code
Bash
npm install drizzle-orm
npm install -D drizzle-kit
TSdb/schema.ts
TypeScript
// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
})

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id').references(() => users.id),
  published: integer('published', { mode: 'boolean' }).default(false),
})
TSdb/index.ts
TypeScript
// db/index.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'

export function createDb(d1: D1Database) {
  return drizzle(d1, { schema })
}

// Usage in a Worker
import { Hono } from 'hono'
import { createDb } from './db'
import { users } from './db/schema'
import { eq } from 'drizzle-orm'

const app = new Hono<{ Bindings: { DB: D1Database } }>()

app.get('/users', async (c) => {
  const db = createDb(c.env.DB)
  const allUsers = await db.select().from(users)
  return c.json(allUsers)
})

app.post('/users', async (c) => {
  const db = createDb(c.env.DB)
  const body = await c.req.json<{ name: string; email: string }>()

  const [newUser] = await db.insert(users).values(body).returning()
  return c.json(newUser, 201)
})

Workers KV

Creating a namespace

Code
Bash
# Create a KV namespace
wrangler kv:namespace create "MY_KV"

# Preview namespace
wrangler kv:namespace create "MY_KV" --preview

KV operations

Code
TypeScript
interface Env {
  KV: KVNamespace
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // GET - read
    const value = await env.KV.get('key')
    const jsonValue = await env.KV.get('user:123', { type: 'json' })
    const binaryValue = await env.KV.get('image', { type: 'arrayBuffer' })

    // PUT - write
    await env.KV.put('key', 'value')

    // PUT with options
    await env.KV.put('session:abc', JSON.stringify({ userId: 1 }), {
      expirationTtl: 3600, // 1 hour
      metadata: { createdAt: Date.now() },
    })

    // PUT with expiration timestamp
    await env.KV.put('temp-key', 'value', {
      expiration: Math.floor(Date.now() / 1000) + 86400, // Unix timestamp
    })

    // DELETE
    await env.KV.delete('key')

    // LIST - listing keys
    const { keys, list_complete, cursor } = await env.KV.list({
      prefix: 'user:',
      limit: 100,
    })

    // GET with metadata
    const { value: val, metadata } = await env.KV.getWithMetadata('key')

    return Response.json({ value, metadata })
  },
}

Cache pattern with KV

Code
TypeScript
async function getCached<T>(
  kv: KVNamespace,
  key: string,
  fetcher: () => Promise<T>,
  ttl = 3600
): Promise<T> {
  // Check cache
  const cached = await kv.get(key, { type: 'json' })
  if (cached) {
    return cached as T
  }

  // Fetch data
  const data = await fetcher()

  // Save to cache
  await kv.put(key, JSON.stringify(data), {
    expirationTtl: ttl,
  })

  return data
}

// Usage
const user = await getCached(
  env.KV,
  `user:${userId}`,
  () => fetchUserFromDB(userId),
  3600
)

R2 - Object Storage

Creating a bucket

Code
Bash
# Create a bucket
wrangler r2 bucket create my-bucket

R2 operations

Code
TypeScript
interface Env {
  BUCKET: R2Bucket
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)
    const key = url.pathname.slice(1)

    switch (request.method) {
      case 'GET': {
        // Retrieve object
        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': {
        // Upload object
        const contentType = request.headers.get('content-type') || 'application/octet-stream'

        await env.BUCKET.put(key, request.body, {
          httpMetadata: {
            contentType,
          },
          customMetadata: {
            uploadedAt: new Date().toISOString(),
          },
        })

        return new Response(`Uploaded ${key}`, { status: 201 })
      }

      case 'DELETE': {
        await env.BUCKET.delete(key)
        return new Response(`Deleted ${key}`, { status: 200 })
      }

      default:
        return new Response('Method not allowed', { status: 405 })
    }
  },
}

Presigned URLs

Code
TypeScript
import { AwsClient } from 'aws4fetch'

interface Env {
  R2_ACCESS_KEY_ID: string
  R2_SECRET_ACCESS_KEY: string
  R2_BUCKET_NAME: string
  R2_ACCOUNT_ID: string
}

async function generatePresignedUrl(
  env: Env,
  key: string,
  expiresIn = 3600
): Promise<string> {
  const client = new AwsClient({
    accessKeyId: env.R2_ACCESS_KEY_ID,
    secretAccessKey: env.R2_SECRET_ACCESS_KEY,
  })

  const endpoint = `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
  const url = new URL(`${endpoint}/${env.R2_BUCKET_NAME}/${key}`)

  const signed = await client.sign(new Request(url, { method: 'GET' }), {
    aws: { signQuery: true },
  })

  return signed.url
}

Image upload endpoint

Code
TypeScript
import { Hono } from 'hono'
import { v4 as uuid } from 'uuid'

const app = new Hono<{ Bindings: { BUCKET: R2Bucket } }>()

app.post('/upload', async (c) => {
  const formData = await c.req.formData()
  const file = formData.get('file') as File | null

  if (!file) {
    return c.json({ error: 'No file provided' }, 400)
  }

  // File type validation
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
  if (!allowedTypes.includes(file.type)) {
    return c.json({ error: 'Invalid file type' }, 400)
  }

  // Size validation (5MB max)
  if (file.size > 5 * 1024 * 1024) {
    return c.json({ error: 'File too large' }, 400)
  }

  // Generate a unique file name
  const ext = file.name.split('.').pop()
  const key = `uploads/${uuid()}.${ext}`

  // Upload to R2
  await c.env.BUCKET.put(key, file.stream(), {
    httpMetadata: {
      contentType: file.type,
    },
  })

  return c.json({
    key,
    url: `https://cdn.example.com/${key}`,
  })
})

export default app

Cloudflare Pages

Deploying a static site

Code
Bash
# From the dist folder
wrangler pages deploy ./dist

# With GitHub (automatic deploys)
# Connect your repo in the Cloudflare Dashboard

Pages with Next.js

Code
Bash
# Add the adapter
npm install @cloudflare/next-on-pages

# Build
npx @cloudflare/next-on-pages

# Deploy
wrangler pages deploy .vercel/output/static
JSnext.config.js
JavaScript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    runtime: 'edge',
  },
}

module.exports = nextConfig

Pages Functions (API routes)

Code
TEXT
project/
├── functions/
│   ├── api/
│   │   ├── users.ts        # /api/users
│   │   └── users/
│   │       └── [id].ts     # /api/users/:id
│   └── _middleware.ts      # Global middleware
├── public/
└── dist/
TSfunctions/api/users.ts
TypeScript
// functions/api/users.ts
interface Env {
  DB: D1Database
}

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const { results } = await context.env.DB
    .prepare('SELECT * FROM users')
    .all()

  return Response.json(results)
}

export const onRequestPost: PagesFunction<Env> = async (context) => {
  const body = await context.request.json<{ name: string; email: string }>()

  const result = await context.env.DB
    .prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
    .bind(body.name, body.email)
    .first()

  return Response.json(result, { status: 201 })
}
TSfunctions/_middleware.ts
TypeScript
// functions/_middleware.ts
export const onRequest: PagesFunction = async (context) => {
  // CORS headers
  const response = await context.next()

  response.headers.set('Access-Control-Allow-Origin', '*')
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
  response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  return response
}

Durable Objects

What are Durable Objects?

Durable Objects are a unique Cloudflare feature that enables stateful computing on the edge. Each Durable Object has:

  • A unique ID
  • Persistent storage (transactional)
  • Single-threaded execution
  • WebSocket support

Counter example

TSsrc/counter.ts
TypeScript
// src/counter.ts
export class Counter implements DurableObject {
  private value: number = 0
  private state: DurableObjectState

  constructor(state: DurableObjectState, env: Env) {
    this.state = state
    // Read the value from storage on startup
    this.state.blockConcurrencyWhile(async () => {
      const stored = await this.state.storage.get<number>('value')
      this.value = stored || 0
    })
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url)

    switch (url.pathname) {
      case '/increment':
        this.value++
        await this.state.storage.put('value', this.value)
        return Response.json({ value: this.value })

      case '/decrement':
        this.value--
        await this.state.storage.put('value', this.value)
        return Response.json({ value: this.value })

      case '/':
        return Response.json({ value: this.value })

      default:
        return new Response('Not found', { status: 404 })
    }
  }
}

// src/index.ts
export { Counter } from './counter'

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)
    const counterId = url.searchParams.get('id') || 'default'

    // Get or create a Durable Object
    const id = env.COUNTER.idFromName(counterId)
    const counter = env.COUNTER.get(id)

    // Forward the request to the Durable Object
    return counter.fetch(request)
  },
}

WebSocket with Durable Objects

Code
TypeScript
export class ChatRoom implements DurableObject {
  private sessions: Map<WebSocket, { username: string }> = new Map()
  private state: DurableObjectState

  constructor(state: DurableObjectState, env: Env) {
    this.state = state
  }

  async fetch(request: Request): Promise<Response> {
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected WebSocket', { status: 426 })
    }

    const { 0: client, 1: server } = new WebSocketPair()

    const username = new URL(request.url).searchParams.get('username') || 'Anonymous'

    this.sessions.set(server, { username })

    server.accept()

    server.addEventListener('message', (event) => {
      const message = {
        username,
        text: event.data,
        timestamp: new Date().toISOString(),
      }

      // Broadcast to everyone
      this.broadcast(JSON.stringify(message))
    })

    server.addEventListener('close', () => {
      this.sessions.delete(server)
      this.broadcast(JSON.stringify({
        type: 'leave',
        username,
      }))
    })

    // Notify about joining
    this.broadcast(JSON.stringify({
      type: 'join',
      username,
    }))

    return new Response(null, {
      status: 101,
      webSocket: client,
    })
  }

  private broadcast(message: string) {
    for (const [ws] of this.sessions) {
      try {
        ws.send(message)
      } catch (e) {
        this.sessions.delete(ws)
      }
    }
  }
}

Queues

Creating a queue

Code
Bash
wrangler queues create my-queue

Producer (sending)

Code
TypeScript
interface Env {
  MY_QUEUE: Queue
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Send a single message
    await env.MY_QUEUE.send({
      type: 'email',
      to: 'user@example.com',
      subject: 'Hello',
      body: 'World',
    })

    // Send a batch
    await env.MY_QUEUE.sendBatch([
      { body: { task: 1 } },
      { body: { task: 2 } },
      { body: { task: 3 } },
    ])

    return new Response('Queued!')
  },
}

Consumer (receiving)

Code
TypeScript
interface Env {
  MY_QUEUE: Queue
}

export default {
  async queue(batch: MessageBatch, env: Env): Promise<void> {
    for (const message of batch.messages) {
      try {
        const data = message.body as { type: string; to: string }

        if (data.type === 'email') {
          await sendEmail(data)
        }

        // Acknowledge processing
        message.ack()
      } catch (error) {
        // Retry later
        message.retry()
      }
    }
  },
}

Scheduled Workers (Cron)

wrangler.toml
TOML
# wrangler.toml
[triggers]
crons = [
  "0 * * * *",      # Every hour
  "0 0 * * *",      # Every day at midnight
  "*/15 * * * *",   # Every 15 minutes
]
Code
TypeScript
export default {
  async scheduled(
    controller: ScheduledController,
    env: Env,
    ctx: ExecutionContext
  ): Promise<void> {
    const triggerTime = controller.scheduledTime

    switch (controller.cron) {
      case '0 * * * *':
        // Hourly task
        await syncData(env)
        break

      case '0 0 * * *':
        // Daily cleanup
        await cleanupOldData(env)
        break

      default:
        console.log(`Unknown cron: ${controller.cron}`)
    }
  },
}

Security

WAF Rules

Code
TypeScript
// Cloudflare Dashboard or API
// Custom WAF rules can be configured through the Dashboard

// In a Worker you can check CF headers
export default {
  async fetch(request: Request): Promise<Response> {
    // Check Bot Score
    const botScore = request.cf?.botManagement?.score

    if (botScore && botScore < 30) {
      return new Response('Bot detected', { status: 403 })
    }

    // Check country
    const country = request.cf?.country

    if (country === 'XX') {
      return new Response('Access denied', { status: 403 })
    }

    // Rate limiting (with KV or Durable Objects)
    const ip = request.headers.get('CF-Connecting-IP')
    // ... rate limiting implementation

    return new Response('OK')
  },
}

Rate limiting with KV

Code
TypeScript
async function rateLimit(
  kv: KVNamespace,
  ip: string,
  limit = 100,
  window = 60
): Promise<boolean> {
  const key = `rate:${ip}`
  const current = await kv.get(key, { type: 'json' }) as {
    count: number
    resetAt: number
  } | null

  const now = Date.now()

  if (!current || current.resetAt < now) {
    await kv.put(key, JSON.stringify({
      count: 1,
      resetAt: now + window * 1000,
    }), { expirationTtl: window })
    return true
  }

  if (current.count >= limit) {
    return false
  }

  await kv.put(key, JSON.stringify({
    count: current.count + 1,
    resetAt: current.resetAt,
  }), { expirationTtl: window })

  return true
}

// Usage
const allowed = await rateLimit(env.KV, ip, 100, 60)
if (!allowed) {
  return new Response('Rate limited', { status: 429 })
}

Pricing

Free tier

  • Workers: 100,000 req/day
  • KV: 100,000 reads/day, 1,000 writes/day
  • D1: 5M rows read/day, 100K rows written/day
  • R2: 10GB storage, no egress fees
  • Pages: Unlimited sites, 500 builds/month

Workers Paid ($5/month minimum)

  • Requests: $0.50 per million after 10M
  • CPU time: Included in request price
  • KV: $0.50 per million reads
  • D1: $0.75 per million rows read
  • R2: $0.015/GB storage, $0 egress

Comparison with AWS

ServiceCloudflareAWS
Compute (1M req)$0.50$0.20 + duration
Storage (10GB)$0.15/mo$0.23/mo
Egress (100GB)$0$9.00
Database reads (1M)$0.75$1.25 (DynamoDB)

Best practices

1. Use Hono for APIs

Code
TypeScript
import { Hono } from 'hono'
const app = new Hono()
// Better routing, middleware, error handling

2. Cache aggressively

Code
TypeScript
const cacheKey = new Request(request.url, request)
const cache = caches.default

let response = await cache.match(cacheKey)
if (!response) {
  response = await handleRequest(request)
  ctx.waitUntil(cache.put(cacheKey, response.clone()))
}

3. Use KV for session/cache

Code
TypeScript
await env.KV.put(`session:${sessionId}`, JSON.stringify(data), {
  expirationTtl: 3600,
})

4. D1 for structured data

Code
TypeScript
// Use prepared statements
const stmt = db.prepare('SELECT * FROM users WHERE id = ?')
const user = await stmt.bind(userId).first()

5. R2 for files

Code
TypeScript
// Zero egress fees!
await env.BUCKET.put(key, file, {
  httpMetadata: { contentType: file.type },
})

FAQ

Workers vs Lambda@Edge?

Workers are faster (zero cold starts), run in more locations (300+ vs 13), and have simpler pricing.

How to migrate from Vercel?

  1. Use @cloudflare/next-on-pages for Next.js
  2. Or rewrite your API to Workers with Hono
  3. Move static files to Pages
  4. Configure a custom domain

Is D1 production-ready?

Yes, since 2024 D1 is GA (Generally Available). It is suitable for production, but for very large databases consider Turso or PlanetScale.

How to debug Workers?

Code
Bash
# Local logs
wrangler dev --local

# Production logs
wrangler tail

Summary

Cloudflare is a complete edge computing platform offering:

  • Workers - Serverless in 300+ locations, zero cold starts
  • D1 - SQLite on the edge
  • R2 - Object storage with no egress fees
  • KV - Key-value storage
  • Pages - Static and SSR hosting
  • Security - WAF, DDoS, Bot Management

Cloudflare is ideal for APIs requiring low latency, global applications, and bandwidth cost optimization.