Skip to content

CDN & Edge -- Cloudflare Pages, Workers, KV & Durable Objects

DodaTech Updated 2026-06-22 9 min read

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.

Build a documentation search endpoint using Cloudflare Workers and KV:

  1. Generate a search index JSON file during the Hugo build
  2. Upload the index to Workers KV using wrangler kv:bulk put
  3. Create a Worker endpoint /api/search that queries KV
  4. Add a search form to the static site that fetches results from the Worker
  5. 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