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
- Global edge - 300+ locations, < 50ms latency globally
- Zero cold starts - Workers start instantly
- Generous free tier - 100k req/day for Workers, 10GB R2
- Integrated ecosystem - Workers + D1 + R2 + KV work together seamlessly
- Security - WAF, DDoS protection, Bot Management built-in
- Developer Experience - Wrangler CLI, local dev environment
Cloudflare vs AWS/Vercel/Netlify
| Feature | Cloudflare | AWS Lambda@Edge | Vercel | Netlify |
|---|---|---|---|---|
| Edge locations | 300+ | 13 (Lambda@Edge) | ~20 | ~20 |
| Cold start | 0ms | 100-500ms | ~50ms | ~100ms |
| Free tier | 100k req/day | Paid | 100k req/mo | 125k req/mo |
| Database | D1 (SQLite) | DynamoDB | Postgres (Neon) | No native |
| Storage | R2 (no egress) | S3 (egress fee) | Blob | None |
| Pricing model | Pay-per-request | Pay-per-duration | Bandwidth | Bandwidth |
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
# 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 honoBasic Worker
// 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
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
// 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)
npm create cloudflare@latest my-api -- --template hono// 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 appLocal development
# 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 8787Deploy
# Deploy to Cloudflare
wrangler deploy
# Deploy to a specific environment
wrangler deploy --env production
# Preview deploy
wrangler deploy --env previewD1 - SQLite on the edge
Creating a D1 database
# Create a database
wrangler d1 create my-database
# Copy the ID to wrangler.toml
# database_id = "xxxxx-xxxx-xxxx-xxxx"Schema and migrations
-- 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);# Run migrations
wrangler d1 migrations apply my-database
# Locally
wrangler d1 migrations apply my-database --localQueries in D1
// 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
npm install drizzle-orm
npm install -D drizzle-kit// 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),
})// 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
# Create a KV namespace
wrangler kv:namespace create "MY_KV"
# Preview namespace
wrangler kv:namespace create "MY_KV" --previewKV operations
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
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
# Create a bucket
wrangler r2 bucket create my-bucketR2 operations
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
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
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 appCloudflare Pages
Deploying a static site
# From the dist folder
wrangler pages deploy ./dist
# With GitHub (automatic deploys)
# Connect your repo in the Cloudflare DashboardPages with Next.js
# Add the adapter
npm install @cloudflare/next-on-pages
# Build
npx @cloudflare/next-on-pages
# Deploy
wrangler pages deploy .vercel/output/static// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
runtime: 'edge',
},
}
module.exports = nextConfigPages Functions (API routes)
project/
├── functions/
│ ├── api/
│ │ ├── users.ts # /api/users
│ │ └── users/
│ │ └── [id].ts # /api/users/:id
│ └── _middleware.ts # Global middleware
├── public/
└── dist/// 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 })
}// 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
// 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
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
wrangler queues create my-queueProducer (sending)
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)
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
[triggers]
crons = [
"0 * * * *", # Every hour
"0 0 * * *", # Every day at midnight
"*/15 * * * *", # Every 15 minutes
]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
// 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
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
| Service | Cloudflare | AWS |
|---|---|---|
| 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
import { Hono } from 'hono'
const app = new Hono()
// Better routing, middleware, error handling2. Cache aggressively
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
await env.KV.put(`session:${sessionId}`, JSON.stringify(data), {
expirationTtl: 3600,
})4. D1 for structured data
// Use prepared statements
const stmt = db.prepare('SELECT * FROM users WHERE id = ?')
const user = await stmt.bind(userId).first()5. R2 for files
// 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?
- Use
@cloudflare/next-on-pagesfor Next.js - Or rewrite your API to Workers with Hono
- Move static files to Pages
- 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?
# Local logs
wrangler dev --local
# Production logs
wrangler tailSummary
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.