CDN & Edge -- Cloudflare Pages, Workers, KV & Durable Objects
In this tutorial, you'll learn about CDN & Edge. We cover key concepts, practical examples, and best practices.
Cloudflare Pages provides global static site hosting with built-in CI/CD, while Workers, KV, and Durable Objects add serverless compute, persistent storage, and real-time coordination at the edge for dynamic capabilities.
What You'll Learn
Why It Matters
Traditional static hosting lacks dynamic capabilities -- form handling, authentication, server-side logic, and persistent storage. Cloudflare's edge platform solves this by offering a global network of 330+ data centers where you can run serverless functions (Workers), store key-value data (KV), and manage real-time state (Durable Objects) without managing any infrastructure. This enables static sites to handle dynamic features while maintaining the speed and security benefits of a JAMstack architecture. At DodaTech, we serve our Hugo site from Pages with Workers handling search API requests and KV caching tutorial metadata.
Real-World Use
A documentation site uses Workers to proxy API requests with authentication headers added at the edge. An e-commerce storefront uses KV to cache product inventory data across all edge locations. A real-time dashboard uses Durable Objects to synchronize WebSocket connections for live updates. Each use case leverages the edge for low latency without provisioning servers.
Edge Architecture on Cloudflare
flowchart LR A[Git Push] --> B[Cloudflare Pages] B --> C[Edge Network 330+ PoPs] C --> D[Static Assets Pages] C --> E[Workers Serverless Functions] E --> F[KV Global Key-Value Store] E --> G[Durable Objects] G --> H[WebSocket State] G --> I[Coordinated State] D --> J[User Browser] E --> J style C fill:#f90,color:#fff
Cloudflare Pages -- Static Hosting
Pages integrates directly with your Git repository and automatically deploys on every push. It supports Hugo, Next.js (static export), Astro, 11ty, and any other SSG that outputs static files.
Pages Configuration
# wrangler.toml -- Cloudflare Pages configuration
name = "dodatech-tutorials"
compatibility_date = "2026-06-01"
pages_build_output_dir = "public"
[build]
command = "hugo --gc --minify"
[build.environment]
HUGO_VERSION = "0.128.0"
HUGO_ENVIRONMENT = "production"
[[redirects]]
from = "/old-path"
to = "/new-path"
status = 301
[[headers]]
for = "/*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=86400"
Expected behavior: On every push to the main branch, Cloudflare runs hugo --gc --minify, outputs to public/, and deploys to all 330+ edge locations. CSS files get immutable caching headers, while asset files get 24-hour cache. Redirects handle URL changes without server configuration.
Cloudflare Workers -- Edge Functions
Workers are serverless functions that run on Cloudflare's edge network. They intercept requests before they reach the static origin, enabling dynamic logic at the edge.
Worker for Search API
// functions/api/search.js -- Edge search worker
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const query = url.searchParams.get('q');
if (!query || query.length < 2) {
return new Response(JSON.stringify({ results: [] }), {
headers: { 'Content-Type': 'application/json' },
});
}
// Search against KV store containing pre-built search index
const searchResults = await env.SEARCH_INDEX.get(query, 'json');
if (searchResults) {
return new Response(JSON.stringify(searchResults), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
},
});
}
// Fallback: search through KV keys matching the query
const keys = await env.SEARCH_INDEX.list({ prefix: query });
const results = [];
for (const key of keys.keys) {
const data = await env.SEARCH_INDEX.get(key.name, 'json');
if (data) results.push(data);
}
return new Response(JSON.stringify({ results }), {
headers: { 'Content-Type': 'application/json' },
});
}
Expected behavior: Requests to /api/search?q=hugo hit the edge worker. If the query is cached in KV, it returns immediately. Otherwise, it lists KV keys by prefix and returns matching results. Responses are cached at the edge for one hour, reducing KV reads.
KV -- Global Key-Value Store
Workers KV is a globally distributed key-value store optimized for high-read, low-write workloads. Data is replicated to all edge locations within 60 seconds.
KV Namespace Configuration
# wrangler.toml -- KV namespace binding
name = "tutorials-api"
[[kv_namespaces]]
binding = "SEARCH_INDEX"
id = "abc123"
// Populating KV from a build script
const CACHE_TTL = 60 * 60 * 24 * 30; // 30 days
async function populateSearchIndex(env) {
// In production, this would iterate over all pages
const pages = [
{ slug: 'hugo-tutorials', title: 'Hugo Tutorials', content: '...' },
{ slug: 'ssg-comparison', title: 'SSG Comparison', content: '...' },
];
for (const page of pages) {
const words = page.content.toLowerCase().split(/\s+/);
for (const word of new Set(words)) {
if (word.length < 2) continue;
// Store each word as a key pointing to matching pages
const existing = await env.SEARCH_INDEX.get(word, 'json') || [];
existing.push({ slug: page.slug, title: page.title });
await env.SEARCH_INDEX.put(word, JSON.stringify(existing), {
expirationTtl: CACHE_TTL,
});
}
}
}
Expected behavior: The script indexes each page's content by word, storing word-to-page mappings in KV with a 30-day TTL. Single lookups return all pages containing that word. Keys are automatically replicated across all edge locations.
Durable Objects -- Real-Time State
Durable Objects provide strongly consistent state storage at a single location, with WebSocket support for real-time communication.
// durable-objects/chat-room.js -- Real-time chat room
export class ChatRoom {
constructor(state, env) {
this.state = state;
this.sessions = [];
this.storage = state.storage;
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.includes('/websocket')) {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
this.sessions.push(server);
server.addEventListener('message', async (event) => {
const message = JSON.parse(event.data);
// Broadcast to all connected sessions
for (const session of this.sessions) {
session.send(JSON.stringify(message));
}
// Persist to storage for history
const history = await this.storage.get('history') || [];
history.push(message);
await this.storage.put('history', history.slice(-100));
});
server.addEventListener('close', () => {
this.sessions = this.sessions.filter(s => s !== server);
});
return new Response(null, { status: 101, webSocket: client });
}
// GET endpoint returns message history
const history = await this.storage.get('history') || [];
return new Response(JSON.stringify(history), {
headers: { 'Content-Type': 'application/json' },
});
}
}
Expected behavior: Each chat room has a unique Durable Object ID. All WebSocket connections to the same room are handled by the same object, ensuring ordered message delivery. Historical messages are persisted and served on page load.
Platform Comparison Table
| Feature | Cloudflare Pages | Netlify | Vercel | AWS Amplify |
|---|---|---|---|---|
| Free tier | Unlimited sites, 500 builds/mo | 100GB bandwidth, 300 build min/mo | 100GB bandwidth, 6000 build min/mo | 1000 build min/mo |
| Edge functions | Workers (global) | Edge Functions (limited) | Edge Functions (limited) | Lambda@Edge |
| KV storage | Workers KV (global) | None | Vercel KV (Redis-based) | DynamoDB |
| Durable Objects | Yes | No | No | No |
| Static export | Any SSG | Any SSG | Next.js (optimized) | Any SSG |
| WebSocket | Yes (DO) | No | No | No |
| Custom domains | Free | Free | Free | Free |
| Global PoPs | 330+ | 30+ | 100+ | 80+ |
Common Errors
1. Cold Starts on Workers
Workers that are infrequently accessed may experience a cold start (JIT compilation). To mitigate, set up a cron trigger that pings the Worker every few minutes, or use the --env production flag with wrangler d1 execute to keep the runtime warm.
2. KV Consistency Guarantees
KV is eventually consistent -- writes may take up to 60 seconds to propagate globally. For use cases requiring strong consistency (e.g., user sessions), use Durable Objects instead.
// BAD: KV for real-time data
await env.SESSION.put(sessionId, data);
// Immediate read on another edge may return stale data
// GOOD: Durable Objects for strong consistency
let session = await env.SESSIONS.get(sessionId);
await session.updateData(data);
3. Exceeding KV Free Tier Limits
KV free tier allows 100,000 reads per day and 1,000 writes per day. High-traffic sites can exceed this quickly. Monitor usage in the Cloudflare dashboard and consider caching frequently accessed keys at the Worker level.
4. Workers context Passing Errors
Forgetting to pass env bindings correctly in middleware chains causes runtime errors. Always destructure context at the top of each handler: export async function onRequest({ request, env, params, waitUntil }).
5. Durable Object Storage Limits
Each Durable Object has a 1GB storage limit. For large datasets, use KV as the primary store and Durable Objects only for coordination. DO storage is also more expensive -- $0.20/GB per month versus $0.50/GB for KV.
Practice Questions
1. What is the main difference between Workers KV and Durable Objects?
KV is eventually consistent and globally distributed, suitable for high-read, low-write scenarios. Durable Objects provide strongly consistent state at a single location, suitable for real-time coordination and WebSocket management.
2. How can you add authentication to static site pages using Workers?
A Worker intercepts requests and checks for a valid token in cookies or headers. If the token is missing or invalid, the Worker returns a 401 or redirects to a login page. The static files are never served without authentication.
3. What is the expiration TTL in KV and what is the maximum allowed?
KV put() accepts expirationTtl in seconds. The maximum TTL is 30 days (2,592,000 seconds). After expiration, the key is automatically deleted.
4. How does Cloudflare Pages handle cache invalidation on new deployment?
On every successful deployment, Cloudflare automatically purges all cached content across all edge locations. The next request to any URL fetches fresh content from the new deployment.
5. Challenge: Build an API key authentication worker that protects a set of static pages.
Create a Worker that checks for a valid API key in the Authorization header. If valid, the request proceeds to the static origin. If invalid, return a 403 with a JSON error message. Store valid keys in KV for easy management.
Mini Project: Edge-Powered Search
Build a documentation search endpoint using Cloudflare Workers and KV:
- Generate a search index JSON file during the Hugo build
- Upload the index to Workers KV using
wrangler kv:bulk put - Create a Worker endpoint
/api/searchthat queries KV - Add a search form to the static site that fetches results from the Worker
- Implement keyboard shortcuts (Ctrl+K) to open search
// functions/api/search.js -- Full search implementation
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const query = url.searchParams.get('q');
if (!query || query.length < 2) {
return Response.json({ results: [] });
}
const cacheKey = `search:${query.toLowerCase()}`;
const cached = await env.CACHE.get(cacheKey);
if (cached) {
return Response.json(JSON.parse(cached));
}
// Tokenize query and search each term
const terms = query.toLowerCase().split(/\s+/);
const resultMap = new Map();
for (const term of terms) {
const data = await env.SEARCH_INDEX.get(term, 'json');
if (data) {
for (const item of data) {
if (!resultMap.has(item.slug)) {
resultMap.set(item.slug, { ...item, score: 0 });
}
resultMap.get(item.slug).score += 1;
}
}
}
const results = Array.from(resultMap.values())
.sort((a, b) => b.score - a.score)
.slice(0, 20);
// Cache for 1 hour
await env.CACHE.put(cacheKey, JSON.stringify(results), {
expirationTtl: 3600,
});
return Response.json({ results });
}
Deploy with wrangler pages deploy and test with curl https://your-site.pages.dev/api/search?q=hugo+templates.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro