Building a blog that consistently delivers content in under 5ms requires strategic architecture decisions and edge-native technologies. This tutorial walks through creating a production-ready blog using Cloudflare Workers, D1 database, and KV cache—achieving global sub-5ms performance without sacrificing functionality.

Architecture Overview

Our edge-native blog architecture leverages three core Cloudflare technologies:

  • Cloudflare Workers: Handle HTTP requests and business logic at the edge
  • D1 Database: SQLite-based database for persistent data storage
  • KV Cache: Distributed key-value store for ultra-fast content delivery

The data flow prioritizes cache hits for maximum performance: KV cache → D1 database → origin, with intelligent cache invalidation ensuring content freshness.

Project Setup and Dependencies

Initialize a new Cloudflare Workers project with the necessary dependencies:

npm create cloudflare@latest blog-workers
cd blog-workers
npm install

Update your wrangler.toml configuration:

name = "edge-blog"
main = "src/index.js"
compatibility_date = "2024-01-15"

[[kv_namespaces]]
binding = "BLOG_CACHE"
preview_id = "your-preview-id"
id = "your-production-id"

[[d1_databases]]
binding = "DB"
database_name = "blog_db"
database_id = "your-database-id"

D1 Database Schema Design

Create a normalized schema optimized for read performance. Generate the database and apply our schema:

npx wrangler d1 create blog_db
npx wrangler d1 execute blog_db --local --file=./schema.sql

Design your schema.sql with performance in mind:

CREATE TABLE posts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  slug TEXT UNIQUE NOT NULL,
  title TEXT NOT NULL,
  content TEXT NOT NULL,
  excerpt TEXT,
  published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  status TEXT DEFAULT 'draft' CHECK(status IN ('draft', 'published'))
);

CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_published ON posts(published_at) WHERE status = 'published';
CREATE INDEX idx_posts_status ON posts(status);

CREATE TABLE tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT UNIQUE NOT NULL,
  slug TEXT UNIQUE NOT NULL
);

CREATE TABLE post_tags (
  post_id INTEGER,
  tag_id INTEGER,
  FOREIGN KEY (post_id) REFERENCES posts(id),
  FOREIGN KEY (tag_id) REFERENCES tags(id),
  PRIMARY KEY (post_id, tag_id)
);

Core Worker Implementation

Build the main Worker that handles routing and implements the caching strategy:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const path = url.pathname;
    
    // Route handling
    if (path === '/' || path === '/blog') {
      return handleBlogList(request, env);
    }
    
    if (path.startsWith('/post/')) {
      const slug = path.replace('/post/', '');
      return handlePost(slug, request, env);
    }
    
    if (path.startsWith('/api/')) {
      return handleAPI(request, env);
    }
    
    return new Response('Not Found', { status: 404 });
  }
};

Implementing the Cache-First Strategy

The cache-first approach ensures sub-5ms response times for cached content:

async function handlePost(slug, request, env) {
  const cacheKey = `post:${slug}`;
  
  // Check KV cache first
  const cached = await env.BLOG_CACHE.get(cacheKey);
  if (cached) {
    const post = JSON.parse(cached);
    return new Response(renderPost(post), {
      headers: {
        'Content-Type': 'text/html',
        'Cache-Control': 'public, max-age=3600',
        'X-Cache': 'HIT'
      }
    });
  }
  
  // Fallback to D1 database
  const post = await getPostFromDB(slug, env.DB);
  if (!post) {
    return new Response('Post not found', { status: 404 });
  }
  
  // Cache for future requests
  await env.BLOG_CACHE.put(cacheKey, JSON.stringify(post), {
    expirationTtl: 3600 // 1 hour
  });
  
  return new Response(renderPost(post), {
    headers: {
      'Content-Type': 'text/html',
      'Cache-Control': 'public, max-age=3600',
      'X-Cache': 'MISS'
    }
  });
}

Database Query Optimization

Optimize D1 queries for minimal latency using prepared statements and efficient indexing:

async function getPostFromDB(slug, db) {
  const stmt = db.prepare(`
    SELECT 
      p.id, p.slug, p.title, p.content, p.excerpt, p.published_at,
      GROUP_CONCAT(t.name) as tags
    FROM posts p
    LEFT JOIN post_tags pt ON p.id = pt.post_id
    LEFT JOIN tags t ON pt.tag_id = t.id
    WHERE p.slug = ? AND p.status = 'published'
    GROUP BY p.id
  `);
  
  const result = await stmt.bind(slug).first();
  
  if (result) {
    result.tags = result.tags ? result.tags.split(',') : [];
  }
  
  return result;
}

async function getPostsList(db, limit = 10, offset = 0) {
  const stmt = db.prepare(`
    SELECT id, slug, title, excerpt, published_at
    FROM posts 
    WHERE status = 'published'
    ORDER BY published_at DESC
    LIMIT ? OFFSET ?
  `);
  
  const results = await stmt.bind(limit, offset).all();
  return results.results;
}

Smart Cache Invalidation

Implement intelligent cache invalidation to maintain content freshness without sacrificing performance:

async function invalidatePostCache(slug, env) {
  const keys = [
    `post:${slug}`,
    'posts:list:0', // First page of blog list
    'posts:list:10', // Second page
    'posts:recent' // Recent posts widget
  ];
  
  await Promise.all(keys.map(key => env.BLOG_CACHE.delete(key)));
}

// Usage in post update/creation
async function updatePost(postData, env) {
  const result = await env.DB.prepare(
    'UPDATE posts SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE slug = ?'
  ).bind(postData.title, postData.content, postData.slug).run();
  
  if (result.success) {
    await invalidatePostCache(postData.slug, env);
  }
  
  return result;
}

HTML Rendering at the Edge

Generate HTML responses directly in the Worker for maximum performance:

function renderPost(post) {
  return `


  
  
  ${escapeHtml(post.title)} | Edge Blog
  
  


  

${escapeHtml(post.title)}

${post.content}
`; } function escapeHtml(text) { const div = new Response().headers; return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }

Performance Monitoring and Optimization

Add performance monitoring to track cache hit rates and response times:

async function logPerformanceMetrics(startTime, cacheStatus, env) {
  const endTime = Date.now();
  const responseTime = endTime - startTime;
  
  // Log to D1 for analytics (async, don't wait)
  env.DB.prepare(
    'INSERT INTO performance_logs (timestamp, response_time, cache_status) VALUES (?, ?, ?)'
  ).bind(new Date().toISOString(), responseTime, cacheStatus).run();
  
  // Set custom headers for monitoring
  return {
    'X-Response-Time': `${responseTime}ms`,
    'X-Cache-Status': cacheStatus
  };
}

Deployment and Testing

Deploy your Cloudflare Workers blog and verify sub-5ms performance:

# Deploy to production
npx wrangler publish

# Test cache performance
curl -H "Cache-Control: no-cache" https://your-worker.your-subdomain.workers.dev/post/example
curl https://your-worker.your-subdomain.workers.dev/post/example

Use Cloudflare Analytics and custom metrics to monitor performance. The first request (cache miss) should complete under 50ms, while subsequent requests (cache hits) should consistently achieve sub-5ms response times globally.

Production Optimizations

Additional optimizations for production deployments:

  • Precompute popular content: Use cron triggers to warm cache for trending posts
  • Implement edge-side includes: Cache page fragments independently for better cache efficiency
  • Add compression: Enable Brotli compression for text content
  • Use custom domains: Reduce DNS lookup time with custom Worker routes

This architecture delivers genuine sub-5ms performance for cached content while maintaining the flexibility to handle dynamic content efficiently through D1 database queries.