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 installUpdate 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.sqlDesign 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/exampleUse 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.