
Cloudflare Workers Performance
- 222 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Use cloudflare-workers-performance for development tasks
About
cloudflare-workers-performance: A skill for development. This provides functionality for development workflows.
- cloudflare-workers-performance
Cloudflare Workers Performance by the numbers
- 222 all-time installs (skills.sh)
- +13 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,798 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workers-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 222 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Use cloudflare-workers-performance for development tasks
Files
Cloudflare Workers Performance Optimization
Techniques for maximizing Worker performance and minimizing latency.
Quick Wins
// 1. Avoid unnecessary cloning
// ❌ Bad: Clones entire request
const body = await request.clone().json();
// ✅ Good: Parse directly when not re-using body
const body = await request.json();
// 2. Use streaming instead of buffering
// ❌ Bad: Buffers entire response
const text = await response.text();
return new Response(transform(text));
// ✅ Good: Stream transformation
return new Response(response.body.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(process(chunk));
}
})));
// 3. Cache expensive operations
const cache = caches.default;
const cached = await cache.match(request);
if (cached) return cached;Critical Rules
1. Stay under CPU limits - 10ms (free), 30ms (paid), 50ms (unbound) 2. Minimize cold starts - Keep bundles < 1MB, avoid dynamic imports 3. Use Cache API - Cache responses at the edge 4. Stream large payloads - Don't buffer entire responses 5. Batch operations - Combine multiple KV/D1 calls
Top 10 Performance Errors
| Error | Symptom | Fix |
|---|---|---|
| CPU limit exceeded | Worker terminated | Optimize hot paths, use streaming |
| Cold start latency | First request slow | Reduce bundle size, avoid top-level await |
| Memory pressure | Slow GC, timeouts | Stream data, avoid large arrays |
| KV latency | Slow reads | Use Cache API, batch reads |
| D1 slow queries | High latency | Add indexes, optimize SQL |
| Large bundles | Slow cold starts | Tree-shake, code split |
| Blocking operations | Request timeouts | Use Promise.all, streaming |
| Unnecessary cloning | Memory spike | Only clone when needed |
| Missing cache | Repeated computation | Implement caching layer |
| Sync operations | CPU spikes | Use async alternatives |
CPU Optimization
Profile Hot Paths
async function profiledHandler(request: Request): Promise<Response> {
const timing: Record<string, number> = {};
const time = async <T>(name: string, fn: () => Promise<T>): Promise<T> => {
const start = Date.now();
const result = await fn();
timing[name] = Date.now() - start;
return result;
};
const data = await time('fetch', () => fetchData());
const processed = await time('process', () => processData(data));
const response = await time('serialize', () => serialize(processed));
console.log('Timing:', timing);
return new Response(response);
}Optimize JSON Operations
// For large JSON, use streaming parser
import { JSONParser } from '@streamparser/json';
async function parseStreamingJSON(stream: ReadableStream): Promise<unknown[]> {
const parser = new JSONParser();
const results: unknown[] = [];
parser.onValue = (value) => results.push(value);
for await (const chunk of stream) {
parser.write(chunk);
}
return results;
}Memory Optimization
Avoid Large Arrays
// ❌ Bad: Loads all into memory
const items = await db.prepare('SELECT * FROM items').all();
const processed = items.results.map(transform);
// ✅ Good: Process in batches
async function* batchProcess(db: D1Database, batchSize = 100) {
let offset = 0;
while (true) {
const { results } = await db
.prepare('SELECT * FROM items LIMIT ? OFFSET ?')
.bind(batchSize, offset)
.all();
if (results.length === 0) break;
for (const item of results) {
yield transform(item);
}
offset += batchSize;
}
}Caching Strategies
Multi-Layer Cache
interface CacheLayer {
get(key: string): Promise<unknown | null>;
set(key: string, value: unknown, ttl?: number): Promise<void>;
}
// Layer 1: In-memory (request-scoped)
const memoryCache = new Map<string, unknown>();
// Layer 2: Cache API (edge-local)
const edgeCache: CacheLayer = {
async get(key) {
const response = await caches.default.match(new Request(`https://cache/${key}`));
return response ? response.json() : null;
},
async set(key, value, ttl = 60) {
await caches.default.put(
new Request(`https://cache/${key}`),
new Response(JSON.stringify(value), {
headers: { 'Cache-Control': `max-age=${ttl}` }
})
);
}
};
// Layer 3: KV (global)
// Use env.KV.get/putBundle Optimization
// 1. Tree-shake imports
// ❌ Bad
import * as lodash from 'lodash';
// ✅ Good
import { debounce } from 'lodash-es';
// 2. Lazy load heavy dependencies
let heavyLib: typeof import('heavy-lib') | undefined;
async function getHeavyLib() {
if (!heavyLib) {
heavyLib = await import('heavy-lib');
}
return heavyLib;
}When to Load References
Load specific references based on the task:
- Optimizing CPU usage? → Load
references/cpu-optimization.md - Memory issues? → Load
references/memory-optimization.md - Implementing caching? → Load
references/caching-strategies.md - Reducing bundle size? → Load
references/bundle-optimization.md - Cold start problems? → Load
references/cold-starts.md
Templates
| Template | Purpose | Use When |
|---|---|---|
templates/performance-middleware.ts | Performance monitoring | Adding timing/profiling |
templates/caching-layer.ts | Multi-layer caching | Implementing cache |
templates/optimized-worker.ts | Performance patterns | Starting optimized worker |
Scripts
| Script | Purpose | Command |
|---|---|---|
scripts/benchmark.sh | Load testing | ./benchmark.sh <url> |
scripts/profile-worker.sh | CPU profiling | ./profile-worker.sh |
Resources
- Performance: https://developers.cloudflare.com/workers/platform/performance/
- Limits: https://developers.cloudflare.com/workers/platform/limits/
- Caching: https://developers.cloudflare.com/workers/runtime-apis/cache/
Bundle Optimization for Cloudflare Workers
Techniques for minimizing bundle size and improving cold start performance.
Bundle Limits
| Metric | Free | Paid |
|---|---|---|
| Worker size (compressed) | 1 MB | 10 MB |
| Worker size (uncompressed) | 3 MB | 30 MB |
Larger bundles = slower cold starts. Target < 1 MB for best performance.
Analyzing Bundle Size
Using Wrangler
# Build and show size
bunx wrangler deploy --dry-run --outdir dist
# Check compressed size
du -h dist/index.js
gzip -c dist/index.js | wc -cBundle Analyzer
// vite.config.ts for analysis
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
plugins: [
visualizer({
filename: 'bundle-stats.html',
gzipSize: true,
brotliSize: true,
}),
],
},
},
});Size Tracking Script
// scripts/check-bundle-size.ts
import { readFileSync, statSync } from 'fs';
import { gzipSync } from 'zlib';
const MAX_SIZE_KB = 500; // Target max size
function checkBundleSize(path: string): void {
const content = readFileSync(path);
const compressed = gzipSync(content);
const rawKB = content.length / 1024;
const gzipKB = compressed.length / 1024;
console.log(`Bundle: ${path}`);
console.log(` Raw: ${rawKB.toFixed(2)} KB`);
console.log(` Gzip: ${gzipKB.toFixed(2)} KB`);
if (gzipKB > MAX_SIZE_KB) {
console.error(` ⚠️ Exceeds target of ${MAX_SIZE_KB} KB!`);
process.exit(1);
} else {
console.log(` ✓ Within target`);
}
}
checkBundleSize('dist/index.js');Tree Shaking
Import Only What You Need
// ❌ Bad: Imports entire library
import * as lodash from 'lodash';
const result = lodash.debounce(fn, 100);
// ✅ Good: Import specific function
import debounce from 'lodash-es/debounce';
const result = debounce(fn, 100);
// ✅ Even better: Use native or lightweight alternative
function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
ms: number
): T {
let timeout: ReturnType<typeof setTimeout>;
return ((...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
}) as T;
}Mark Side Effects
// package.json - helps bundler with tree shaking
{
"sideEffects": false,
// or specify files with side effects
"sideEffects": ["*.css", "./src/polyfills.ts"]
}Named Exports for Better Tree Shaking
// ❌ Bad: Default export object
export default {
functionA,
functionB,
functionC,
};
// ✅ Good: Named exports
export { functionA, functionB, functionC };Code Splitting
Dynamic Imports
// Heavy code loaded only when needed
async function processImage(image: ArrayBuffer): Promise<ArrayBuffer> {
// Only load image processing when needed
const { processImage } = await import('./heavy-image-lib');
return processImage(image);
}
// Route-based splitting
const routes: Record<string, () => Promise<{ default: Handler }>> = {
'/api/images': () => import('./handlers/images'),
'/api/pdf': () => import('./handlers/pdf'),
'/api/export': () => import('./handlers/export'),
};
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
for (const [path, loader] of Object.entries(routes)) {
if (url.pathname.startsWith(path)) {
const { default: handler } = await loader();
return handler(request);
}
}
return new Response('Not Found', { status: 404 });
}
};Feature Flags for Conditional Loading
interface Env {
ENABLE_AI: string;
ENABLE_ANALYTICS: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Only load AI module if enabled
if (env.ENABLE_AI === 'true') {
const ai = await import('./features/ai');
// Use AI features
}
// Only load analytics if enabled
if (env.ENABLE_ANALYTICS === 'true') {
const analytics = await import('./features/analytics');
// Use analytics
}
return handleRequest(request);
}
};Dependency Optimization
Lightweight Alternatives
| Heavy Package | Lightweight Alternative |
|---|---|
| lodash (70kb) | lodash-es (tree-shakeable) or native |
| moment (300kb) | dayjs (2kb) or Temporal |
| uuid (10kb) | crypto.randomUUID() (native) |
| axios (40kb) | fetch (native) |
| validator (60kb) | Custom validation or zod |
| bluebird (80kb) | Native Promises |
Audit Dependencies
# Check bundle contribution of each package
npx bundle-buddy dist/stats.json
# Find unused dependencies
npx depcheck
# Check for duplicates
npx npm-dedupeExternal Dependencies
// wrangler.jsonc - exclude from bundle
{
"build": {
"command": "bun run build"
},
"rules": [
{
"type": "ESModule",
"globs": ["**/*.js"],
"fallthrough": true
}
]
}Minification
Configure Minification
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
passes: 2,
drop_console: true, // Remove console.log in production
drop_debugger: true,
pure_funcs: ['console.log', 'console.debug'],
},
mangle: true,
},
},
});Remove Dead Code
// Use environment flags
const IS_DEV = process.env.NODE_ENV === 'development';
function debug(...args: unknown[]): void {
if (IS_DEV) {
console.log('[DEBUG]', ...args);
}
}
// After minification with dead code elimination:
// debug() calls are completely removed in productionCompression
Pre-compress Assets
# Gzip compression
gzip -k -9 dist/index.js
# Brotli compression (better)
brotli -k -q 11 dist/index.jsOptimize for Compression
// Compression-friendly patterns
// ❌ Bad: Unique strings
const errors = {
E001: 'Invalid user input',
E002: 'Database connection failed',
E003: 'Authentication required',
};
// ✅ Good: Repetitive patterns compress better
const ERROR_PREFIX = 'Error: ';
const errors = {
E001: ERROR_PREFIX + 'Invalid user input',
E002: ERROR_PREFIX + 'Database connection failed',
E003: ERROR_PREFIX + 'Authentication required',
};Asset Optimization
Inline Small Assets
// Inline small SVGs instead of importing
const ArrowIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/>
</svg>`;
// Use data URLs for tiny images (< 1KB)
const tinyImage = 'data:image/png;base64,iVBORw0KGgo...';Optimize JSON Data
// ❌ Bad: Verbose JSON
const config = {
"enableFeatureA": true,
"enableFeatureB": false,
"maxRetries": 3,
"timeout": 5000,
};
// ✅ Good: Compact representation
const config = { a: 1, b: 0, r: 3, t: 5000 };
// Or use binary format for large data
const binaryConfig = new Uint8Array([1, 0, 3, 0, 0, 19, 136]); // PackedBuild Pipeline
Optimized Build Script
{
"scripts": {
"build": "bun run build:bundle && bun run build:analyze",
"build:bundle": "esbuild src/index.ts --bundle --minify --format=esm --outfile=dist/index.js",
"build:analyze": "bun scripts/check-bundle-size.ts",
"build:watch": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --watch"
}
}ESBuild Configuration
// build.ts
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
format: 'esm',
target: 'es2022',
platform: 'browser',
outfile: 'dist/index.js',
treeShaking: true,
drop: ['console', 'debugger'],
define: {
'process.env.NODE_ENV': '"production"',
},
external: [
// Cloudflare-provided modules
'cloudflare:email',
'cloudflare:sockets',
],
});Monitoring Bundle Size
CI Check
# .github/workflows/bundle-check.yml
name: Bundle Size Check
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun run build
- name: Check bundle size
run: |
SIZE=$(gzip -c dist/index.js | wc -c)
echo "Bundle size: $SIZE bytes"
if [ $SIZE -gt 512000 ]; then
echo "Bundle exceeds 500KB limit!"
exit 1
fiCaching Strategies for Cloudflare Workers
Comprehensive guide to caching at the edge for maximum performance.
Cache Layers
Request → Memory Cache → Edge Cache (Cache API) → KV → Origin
(fastest) (per-colo) (global) (slowest)| Layer | Latency | Scope | TTL | Use Case |
|---|---|---|---|---|
| Memory | <1ms | Request | Request duration | Computed values |
| Cache API | 1-5ms | Per colo | Custom | HTTP responses |
| KV | 10-50ms | Global | Custom | Persistent data |
Cache API
Basic Usage
const cache = caches.default;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Check cache
const cached = await cache.match(request);
if (cached) {
return cached;
}
// Generate response
const response = await generateResponse(request, env);
// Cache response (non-blocking)
const cacheableResponse = new Response(response.body, response);
cacheableResponse.headers.set('Cache-Control', 'max-age=3600');
// Don't await - cache in background
cache.put(request, cacheableResponse.clone());
return response;
}
};Custom Cache Keys
function getCacheKey(request: Request): Request {
const url = new URL(request.url);
// Normalize URL
url.searchParams.sort();
// Remove tracking params
url.searchParams.delete('utm_source');
url.searchParams.delete('utm_medium');
url.searchParams.delete('utm_campaign');
// Include important headers in key
const headers = new Headers();
headers.set('Accept', request.headers.get('Accept') || '*/*');
return new Request(url.toString(), {
method: 'GET',
headers,
});
}
// Usage
const cacheKey = getCacheKey(request);
const cached = await cache.match(cacheKey);Vary-Based Caching
async function cacheWithVary(
request: Request,
response: Response,
varyHeaders: string[]
): Promise<void> {
const cache = caches.default;
// Create unique cache key based on Vary headers
const url = new URL(request.url);
for (const header of varyHeaders) {
const value = request.headers.get(header);
if (value) {
url.searchParams.set(`_vary_${header}`, value);
}
}
const cacheKey = new Request(url.toString());
const cacheableResponse = new Response(response.body, response);
cacheableResponse.headers.set('Vary', varyHeaders.join(', '));
await cache.put(cacheKey, cacheableResponse);
}Stale-While-Revalidate
interface SWROptions {
cache: Cache;
maxAge: number;
staleWhileRevalidate: number;
}
async function fetchWithSWR(
request: Request,
fetcher: () => Promise<Response>,
options: SWROptions
): Promise<Response> {
const { cache, maxAge, staleWhileRevalidate } = options;
const cached = await cache.match(request);
if (cached) {
const age = getAge(cached);
// Fresh - return immediately
if (age < maxAge) {
return cached;
}
// Stale but within revalidate window
if (age < maxAge + staleWhileRevalidate) {
// Return stale, revalidate in background
revalidate(request, fetcher, cache, maxAge);
return cached;
}
}
// No cache or too stale - fetch fresh
const response = await fetcher();
await cacheResponse(request, response.clone(), cache, maxAge);
return response;
}
function getAge(response: Response): number {
const date = response.headers.get('Date');
if (!date) return Infinity;
return (Date.now() - new Date(date).getTime()) / 1000;
}
async function revalidate(
request: Request,
fetcher: () => Promise<Response>,
cache: Cache,
maxAge: number
): Promise<void> {
try {
const response = await fetcher();
await cacheResponse(request, response, cache, maxAge);
} catch {
// Keep serving stale on revalidation failure
}
}
async function cacheResponse(
request: Request,
response: Response,
cache: Cache,
maxAge: number
): Promise<void> {
const cached = new Response(response.body, response);
cached.headers.set('Cache-Control', `max-age=${maxAge}`);
cached.headers.set('Date', new Date().toUTCString());
await cache.put(request, cached);
}KV-Based Caching
Simple KV Cache
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class KVCache<T> {
constructor(
private kv: KVNamespace,
private prefix = 'cache:'
) {}
async get(key: string): Promise<T | null> {
const entry = await this.kv.get<CacheEntry<T>>(
this.prefix + key,
'json'
);
if (!entry) return null;
// Check expiration
if (Date.now() > entry.timestamp + entry.ttl * 1000) {
// Expired - delete in background
this.kv.delete(this.prefix + key);
return null;
}
return entry.data;
}
async set(key: string, data: T, ttlSeconds = 3600): Promise<void> {
const entry: CacheEntry<T> = {
data,
timestamp: Date.now(),
ttl: ttlSeconds,
};
await this.kv.put(this.prefix + key, JSON.stringify(entry), {
expirationTtl: ttlSeconds,
});
}
async delete(key: string): Promise<void> {
await this.kv.delete(this.prefix + key);
}
}Cache-Aside Pattern
class CacheAside<T> {
constructor(
private cache: KVCache<T>,
private ttl: number
) {}
async get(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
// Try cache first
const cached = await this.cache.get(key);
if (cached !== null) {
return cached;
}
// Cache miss - fetch data
const data = await fetcher();
// Store in cache (non-blocking)
this.cache.set(key, data, this.ttl);
return data;
}
async invalidate(key: string): Promise<void> {
await this.cache.delete(key);
}
}
// Usage
const userCache = new CacheAside<User>(kvCache, 3600);
const user = await userCache.get(`user:${userId}`, async () => {
return db.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
});Multi-Layer Cache
interface CacheLayer<T> {
get(key: string): Promise<T | null>;
set(key: string, value: T, ttl?: number): Promise<void>;
}
class MultiLayerCache<T> {
private layers: CacheLayer<T>[];
constructor(layers: CacheLayer<T>[]) {
this.layers = layers;
}
async get(key: string): Promise<T | null> {
for (let i = 0; i < this.layers.length; i++) {
const value = await this.layers[i].get(key);
if (value !== null) {
// Populate upper layers
for (let j = 0; j < i; j++) {
this.layers[j].set(key, value);
}
return value;
}
}
return null;
}
async set(key: string, value: T, ttl?: number): Promise<void> {
await Promise.all(
this.layers.map(layer => layer.set(key, value, ttl))
);
}
}
// Memory layer
class MemoryCache<T> implements CacheLayer<T> {
private cache = new Map<string, { value: T; expires: number }>();
async get(key: string): Promise<T | null> {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.value;
}
async set(key: string, value: T, ttl = 60): Promise<void> {
this.cache.set(key, {
value,
expires: Date.now() + ttl * 1000,
});
}
}
// Edge cache layer
class EdgeCache<T> implements CacheLayer<T> {
private cache = caches.default;
async get(key: string): Promise<T | null> {
const response = await this.cache.match(new Request(`https://cache/${key}`));
return response ? response.json() : null;
}
async set(key: string, value: T, ttl = 3600): Promise<void> {
await this.cache.put(
new Request(`https://cache/${key}`),
new Response(JSON.stringify(value), {
headers: { 'Cache-Control': `max-age=${ttl}` },
})
);
}
}
// Usage
const cache = new MultiLayerCache<User>([
new MemoryCache(), // Fast, request-scoped
new EdgeCache(), // Medium, per-colo
new KVCache(env.KV) // Slow, global
]);Cache Invalidation
Tag-Based Invalidation
interface TaggedCacheEntry<T> {
data: T;
tags: string[];
}
class TaggedCache<T> {
constructor(private kv: KVNamespace) {}
async set(key: string, data: T, tags: string[], ttl = 3600): Promise<void> {
// Store entry
await this.kv.put(
`entry:${key}`,
JSON.stringify({ data, tags }),
{ expirationTtl: ttl }
);
// Update tag indexes
for (const tag of tags) {
const tagKeys = await this.kv.get<string[]>(`tag:${tag}`, 'json') || [];
if (!tagKeys.includes(key)) {
tagKeys.push(key);
await this.kv.put(`tag:${tag}`, JSON.stringify(tagKeys));
}
}
}
async invalidateByTag(tag: string): Promise<void> {
const tagKeys = await this.kv.get<string[]>(`tag:${tag}`, 'json') || [];
await Promise.all(
tagKeys.map(key => this.kv.delete(`entry:${key}`))
);
await this.kv.delete(`tag:${tag}`);
}
}
// Usage
await taggedCache.set('user:123', userData, ['users', 'team:456']);
// Invalidate all users
await taggedCache.invalidateByTag('users');Purge API
async function purgeCache(urls: string[], apiToken: string, zoneId: string): Promise<void> {
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ files: urls }),
}
);
if (!response.ok) {
throw new Error(`Cache purge failed: ${response.statusText}`);
}
}Cache Headers
Setting Cache Headers
function setCacheHeaders(
response: Response,
options: {
maxAge?: number;
sMaxAge?: number;
staleWhileRevalidate?: number;
staleIfError?: number;
private?: boolean;
noStore?: boolean;
}
): Response {
const newResponse = new Response(response.body, response);
const directives: string[] = [];
if (options.noStore) {
directives.push('no-store');
} else {
if (options.private) {
directives.push('private');
} else {
directives.push('public');
}
if (options.maxAge !== undefined) {
directives.push(`max-age=${options.maxAge}`);
}
if (options.sMaxAge !== undefined) {
directives.push(`s-maxage=${options.sMaxAge}`);
}
if (options.staleWhileRevalidate !== undefined) {
directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
}
if (options.staleIfError !== undefined) {
directives.push(`stale-if-error=${options.staleIfError}`);
}
}
newResponse.headers.set('Cache-Control', directives.join(', '));
return newResponse;
}
// Usage
const response = setCacheHeaders(originalResponse, {
maxAge: 60, // Browser cache: 1 minute
sMaxAge: 3600, // CDN cache: 1 hour
staleWhileRevalidate: 86400, // Serve stale for 1 day while revalidating
staleIfError: 86400, // Serve stale for 1 day on origin error
});Cold Start Optimization for Cloudflare Workers
Techniques for minimizing cold start latency and improving first-request performance.
Understanding Cold Starts
Cold starts occur when: 1. First request to a new colo (data center) 2. Worker hasn't been used recently 3. After deployment 4. Isolate recycling due to memory pressure
Cold Start Anatomy
Request → DNS → Edge → Isolate Creation → V8 Initialization → Module Evaluation → Handler Execution
└─────────────────── Cold Start Time ───────────────────┘Typical cold start: 5-50ms depending on bundle size and initialization.
Measuring Cold Starts
Server-Timing Header
let isWarm = false;
export default {
async fetch(request: Request): Promise<Response> {
const start = performance.now();
const wasCold = !isWarm;
isWarm = true;
const response = await handleRequest(request);
// Add timing info
const newResponse = new Response(response.body, response);
const handlerTime = performance.now() - start;
newResponse.headers.set(
'Server-Timing',
`handler;dur=${handlerTime.toFixed(2)}, cold;desc="${wasCold}"`
);
return newResponse;
}
};Cold Start Logging
interface StartupMetrics {
isCold: boolean;
startupTime?: number;
moduleLoadTime?: number;
}
const moduleLoadStart = Date.now();
// Track module initialization
const moduleLoadTime = Date.now() - moduleLoadStart;
let isolateStartTime: number | undefined;
let isFirstRequest = true;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const requestStart = Date.now();
const metrics: StartupMetrics = { isCold: false };
if (isFirstRequest) {
isFirstRequest = false;
isolateStartTime = requestStart;
metrics.isCold = true;
metrics.moduleLoadTime = moduleLoadTime;
}
const response = await handleRequest(request, env);
if (metrics.isCold) {
metrics.startupTime = Date.now() - requestStart;
console.log('Cold start metrics:', metrics);
}
return response;
}
};Reducing Cold Start Time
Minimize Top-Level Code
// ❌ Bad: Heavy initialization at module load
import { parse } from 'yaml';
import { readFileSync } from 'fs';
const config = parse(readFileSync('./config.yaml', 'utf8'));
const cache = new Map(Object.entries(config.cache));
const validators = Object.keys(config.routes).map(createValidator);
// ✅ Good: Lazy initialization
let config: Config | undefined;
let cache: Map<string, unknown> | undefined;
let validators: Validator[] | undefined;
function getConfig(): Config {
if (!config) {
config = JSON.parse(CONFIG_JSON); // Use JSON instead of YAML
}
return config;
}
function getCache(): Map<string, unknown> {
if (!cache) {
cache = new Map();
}
return cache;
}Defer Heavy Imports
// ❌ Bad: Import at top level
import { createCanvas, Image } from 'canvas';
import sharp from 'sharp';
// ✅ Good: Import when needed
let sharp: typeof import('sharp') | undefined;
async function processImage(input: ArrayBuffer): Promise<ArrayBuffer> {
if (!sharp) {
sharp = await import('sharp');
}
return sharp.default(input)
.resize(800, 600)
.toBuffer();
}Singleton Pattern for Expensive Objects
// Lazy singleton pattern
class DatabaseConnection {
private static instance: DatabaseConnection | undefined;
private constructor(private db: D1Database) {}
static getInstance(db: D1Database): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection(db);
}
return DatabaseConnection.instance;
}
async query(sql: string, params: unknown[]): Promise<unknown[]> {
return this.db.prepare(sql).bind(...params).all();
}
}Avoid Synchronous Operations
// ❌ Bad: Sync operations block startup
const data = JSON.parse(largeJsonString); // Blocks
const sorted = data.sort((a, b) => a.name.localeCompare(b.name)); // Blocks
// ✅ Good: Async initialization
let sortedData: Data[] | undefined;
async function getSortedData(): Promise<Data[]> {
if (!sortedData) {
// Parse and sort on first request, not module load
const data = JSON.parse(largeJsonString);
sortedData = data.sort((a, b) => a.name.localeCompare(b.name));
}
return sortedData;
}Bundle Size Impact
Size vs Cold Start Time
| Bundle Size | Typical Cold Start |
|---|---|
| < 100 KB | 5-10ms |
| 100-500 KB | 10-20ms |
| 500 KB - 1 MB | 20-40ms |
| > 1 MB | 40-100ms+ |
Minimize Bundle
// esbuild.config.ts
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
treeShaking: true,
format: 'esm',
target: 'es2022',
drop: ['console', 'debugger'],
mangleProps: /^_/, // Mangle private properties
outfile: 'dist/index.js',
});Warming Strategies
Cron-Based Warming
// Keep worker warm with scheduled requests
export default {
async fetch(request: Request): Promise<Response> {
return handleRequest(request);
},
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
// Called by cron trigger every 5 minutes
// Just accessing the worker keeps it warm
// Optionally pre-warm caches
ctx.waitUntil(prewarmCaches(env));
},
};
// wrangler.jsonc
// "triggers": {
// "crons": ["*/5 * * * *"] // Every 5 minutes
// }Service Binding Warming
// From main worker, periodically call dependent workers
async function warmServices(env: Env): Promise<void> {
await Promise.all([
env.AUTH_SERVICE.fetch(new Request('http://internal/health')),
env.DATA_SERVICE.fetch(new Request('http://internal/health')),
env.CACHE_SERVICE.fetch(new Request('http://internal/health')),
]);
}Multi-Colo Warming
// Warm worker in multiple colos
async function warmGlobally(workerUrl: string, locations: string[]): Promise<void> {
// Use Cloudflare Durable Objects or external service
// to make requests from different regions
for (const location of locations) {
// Request through specific colo
await fetch(workerUrl, {
headers: {
'CF-Preferred-Colo': location, // Not a real header - illustration only
},
});
}
}Isolate Reuse
Global State Considerations
// Global state persists across requests in the same isolate
let requestCount = 0;
let lastRequestTime = 0;
export default {
async fetch(request: Request): Promise<Response> {
requestCount++;
lastRequestTime = Date.now();
// ⚠️ Be careful with global state
// - Don't store request-specific data
// - Don't store sensitive information
// - Do use for caching and connection pooling
return handleRequest(request);
}
};Connection Pooling
// Reuse connections across requests
let dbConnection: DatabaseConnection | undefined;
function getConnection(env: Env): DatabaseConnection {
if (!dbConnection) {
dbConnection = new DatabaseConnection(env.DB);
}
return dbConnection;
}
// Will be reused for subsequent requests in same isolatePreloading
Preload Common Data
// Preload data likely to be needed
let commonData: CommonData | undefined;
async function preload(env: Env): Promise<void> {
if (!commonData) {
// Load common data once per isolate
commonData = {
config: await env.KV.get('config', 'json'),
translations: await env.KV.get('translations', 'json'),
routes: await env.KV.get('routes', 'json'),
};
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Preload in background on first request
ctx.waitUntil(preload(env));
return handleRequest(request, env);
}
};Speculative Preloading
// Predict and preload based on request patterns
const prefetchMap = new Map<string, string[]>([
['/products', ['/api/categories', '/api/featured']],
['/checkout', ['/api/payment-methods', '/api/shipping']],
]);
async function speculativePrefetch(
path: string,
env: Env
): Promise<void> {
const toPrefetch = prefetchMap.get(path);
if (!toPrefetch) return;
await Promise.all(
toPrefetch.map(async (url) => {
// Prefetch and cache
const response = await fetch(url);
await caches.default.put(new Request(url), response);
})
);
}Monitoring Cold Starts
Analytics Engine Tracking
interface ColdStartMetric {
timestamp: number;
isCold: boolean;
startupMs: number;
bundleSize: number;
colo: string;
}
function trackColdStart(
analytics: AnalyticsEngineDataset,
metrics: ColdStartMetric
): void {
analytics.writeDataPoint({
indexes: [metrics.colo],
blobs: [metrics.isCold ? 'cold' : 'warm'],
doubles: [metrics.startupMs, metrics.bundleSize],
});
}
// Query cold start metrics
// SELECT
// blob1 as start_type,
// AVG(double1) as avg_startup_ms,
// COUNT(*) as count
// FROM worker_cold_starts
// WHERE timestamp > now() - interval '1' hour
// GROUP BY start_typeCPU Optimization for Cloudflare Workers
Techniques for staying within CPU time limits and maximizing throughput.
Understanding CPU Limits
| Plan | CPU Time Limit | Use Case |
|---|---|---|
| Free | 10ms | Simple transformations |
| Paid (Bundled) | 50ms | Complex processing |
| Paid (Unbound) | 30s (soft) | Long-running tasks |
CPU time is actual execution time, not wall-clock time. Waiting for I/O doesn't count.
Profiling CPU Usage
Basic Timing
class CPUProfiler {
private marks: Map<string, number> = new Map();
private measures: Map<string, number[]> = new Map();
mark(name: string): void {
this.marks.set(name, performance.now());
}
measure(name: string, startMark: string): number {
const start = this.marks.get(startMark);
if (!start) throw new Error(`Mark ${startMark} not found`);
const duration = performance.now() - start;
const existing = this.measures.get(name) || [];
existing.push(duration);
this.measures.set(name, existing);
return duration;
}
getSummary(): Record<string, { count: number; total: number; avg: number; max: number }> {
const summary: Record<string, { count: number; total: number; avg: number; max: number }> = {};
for (const [name, times] of this.measures) {
summary[name] = {
count: times.length,
total: times.reduce((a, b) => a + b, 0),
avg: times.reduce((a, b) => a + b, 0) / times.length,
max: Math.max(...times),
};
}
return summary;
}
reset(): void {
this.marks.clear();
this.measures.clear();
}
}
// Usage
const profiler = new CPUProfiler();
export default {
async fetch(request: Request, env: Env): Promise<Response> {
profiler.mark('start');
profiler.mark('parse-start');
const data = await request.json();
profiler.measure('parse', 'parse-start');
profiler.mark('process-start');
const result = processData(data);
profiler.measure('process', 'process-start');
profiler.measure('total', 'start');
console.log('CPU Profile:', profiler.getSummary());
return Response.json(result);
}
};Async Profiling with Context
interface ProfileContext {
requestId: string;
timings: Record<string, number>;
}
async function withProfiling<T>(
ctx: ProfileContext,
name: string,
fn: () => Promise<T>
): Promise<T> {
const start = performance.now();
try {
return await fn();
} finally {
ctx.timings[name] = performance.now() - start;
}
}
// Usage
const ctx: ProfileContext = {
requestId: crypto.randomUUID(),
timings: {},
};
const user = await withProfiling(ctx, 'fetchUser', () =>
db.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first()
);
const orders = await withProfiling(ctx, 'fetchOrders', () =>
db.prepare('SELECT * FROM orders WHERE user_id = ?').bind(userId).all()
);
console.log(`[${ctx.requestId}] Timings:`, ctx.timings);CPU-Intensive Operation Patterns
Batch Processing
// ❌ Bad: Process one at a time
for (const item of items) {
await processItem(item);
}
// ✅ Good: Process in parallel batches
async function processBatch<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
batchSize = 10
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
}
return results;
}Chunked Processing with Yielding
// For very long arrays, yield to event loop
async function processWithYield<T, R>(
items: T[],
processor: (item: T) => R,
chunkSize = 100
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
results.push(...chunk.map(processor));
// Yield to event loop every chunk
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}Memoization
// In-request memoization
function memoize<T extends (...args: unknown[]) => unknown>(
fn: T,
keyFn?: (...args: Parameters<T>) => string
): T {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>) => {
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result as ReturnType<T>);
return result;
}) as T;
}
// Usage
const expensiveCalculation = memoize((x: number, y: number) => {
// Expensive computation
return x ** y;
});Optimizing Common Operations
String Operations
// ❌ Bad: String concatenation in loop
let result = '';
for (const item of items) {
result += item.toString() + ',';
}
// ✅ Good: Use array join
const result = items.map(item => item.toString()).join(',');
// ✅ Even better for large strings: Use TextEncoder
const encoder = new TextEncoder();
const parts: Uint8Array[] = items.map(item =>
encoder.encode(item.toString())
);JSON Operations
// ❌ Bad: Parse then stringify
const obj = JSON.parse(jsonString);
obj.newField = 'value';
const result = JSON.stringify(obj);
// ✅ Good: For simple additions, use string manipulation
const result = jsonString.slice(0, -1) + ',"newField":"value"}';
// ✅ Best: Use streaming for large JSON
import { JSONParser } from '@streamparser/json';
async function processLargeJSON(stream: ReadableStream): Promise<void> {
const parser = new JSONParser({ paths: ['$.items.*'] });
parser.onValue = (value, key, parent, stack) => {
// Process each item as it's parsed
processItem(value);
};
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
parser.write(value);
}
}Regular Expressions
// ❌ Bad: Create regex in hot path
function findMatches(text: string, pattern: string): string[] {
const regex = new RegExp(pattern, 'g');
return text.match(regex) || [];
}
// ✅ Good: Pre-compile regex
const EMAIL_REGEX = /[\w.-]+@[\w.-]+\.\w+/g;
function findEmails(text: string): string[] {
return text.match(EMAIL_REGEX) || [];
}
// ✅ Even better: Use simpler string methods when possible
function containsKeyword(text: string, keyword: string): boolean {
// indexOf is faster than regex for simple checks
return text.toLowerCase().indexOf(keyword.toLowerCase()) !== -1;
}Object Operations
// ❌ Bad: Spread operator for large objects
const merged = { ...largeObj1, ...largeObj2 };
// ✅ Good: Object.assign for mutation
const result = Object.assign({}, largeObj1, largeObj2);
// ✅ Even better: Mutate if original not needed
Object.assign(largeObj1, largeObj2);
// ❌ Bad: Object.keys().forEach for iteration
Object.keys(obj).forEach(key => {
process(obj[key]);
});
// ✅ Good: for...in with hasOwnProperty
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
process(obj[key]);
}
}Avoiding CPU Spikes
Spread Operations Out
// ❌ Bad: All at once
const results = await Promise.all(
thousandsOfItems.map(async item => heavyProcessing(item))
);
// ✅ Good: Rate-limited processing
async function rateLimitedProcess<T, R>(
items: T[],
processor: (item: T) => Promise<R>,
concurrency = 5
): Promise<R[]> {
const results: R[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = processor(item).then(result => {
results.push(result);
});
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
executing.splice(
executing.findIndex(e => e === p),
1
);
}
}
await Promise.all(executing);
return results;
}Early Returns
// ❌ Bad: Process everything then filter
const processed = items.map(item => expensiveTransform(item));
const filtered = processed.filter(item => item.isValid);
// ✅ Good: Filter first, then process
const valid = items.filter(item => quickValidation(item));
const processed = valid.map(item => expensiveTransform(item));
// ✅ Even better: Short-circuit when possible
function processUntilLimit(items: Item[], limit: number): Result[] {
const results: Result[] = [];
for (const item of items) {
if (results.length >= limit) break;
const result = process(item);
if (result.isValid) {
results.push(result);
}
}
return results;
}CPU Time Monitoring
Add Timing Headers
function addTimingHeaders(response: Response, timings: Record<string, number>): Response {
const newResponse = new Response(response.body, response);
// Server-Timing header for DevTools
const serverTiming = Object.entries(timings)
.map(([name, duration]) => `${name};dur=${duration}`)
.join(', ');
newResponse.headers.set('Server-Timing', serverTiming);
return newResponse;
}Logging Slow Requests
const SLOW_THRESHOLD_MS = 20;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const start = performance.now();
try {
return await handleRequest(request, env);
} finally {
const duration = performance.now() - start;
if (duration > SLOW_THRESHOLD_MS) {
console.warn('Slow request:', {
url: request.url,
method: request.method,
duration: `${duration.toFixed(2)}ms`,
});
}
}
}
};Memory Optimization for Cloudflare Workers
Techniques for efficient memory usage within Worker limits.
Memory Limits
| Plan | Memory Limit |
|---|---|
| Free | 128 MB |
| Paid | 128 MB |
The 128 MB limit is per-isolate, shared across all requests in that isolate.
Memory Profiling
Estimate Object Size
function estimateSize(obj: unknown, seen = new WeakSet()): number {
if (obj === null || obj === undefined) return 0;
const type = typeof obj;
if (type === 'boolean') return 4;
if (type === 'number') return 8;
if (type === 'string') return (obj as string).length * 2;
if (type !== 'object') return 0;
// Avoid circular references
if (seen.has(obj as object)) return 0;
seen.add(obj as object);
if (obj instanceof ArrayBuffer) {
return obj.byteLength;
}
if (ArrayBuffer.isView(obj)) {
return obj.byteLength;
}
if (Array.isArray(obj)) {
return obj.reduce((sum, item) => sum + estimateSize(item, seen), 0);
}
// Regular object
return Object.entries(obj).reduce(
(sum, [key, value]) => sum + key.length * 2 + estimateSize(value, seen),
0
);
}
// Usage
const data = await response.json();
const sizeKB = estimateSize(data) / 1024;
console.log(`Response size: ${sizeKB.toFixed(2)} KB`);
if (sizeKB > 1000) {
console.warn('Large response detected, consider streaming');
}Track Memory Usage
interface MemoryTracker {
allocations: Map<string, number>;
track(label: string, size: number): void;
release(label: string): void;
getSummary(): { total: number; breakdown: Record<string, number> };
}
function createMemoryTracker(): MemoryTracker {
const allocations = new Map<string, number>();
return {
allocations,
track(label: string, size: number): void {
const current = allocations.get(label) || 0;
allocations.set(label, current + size);
},
release(label: string): void {
allocations.delete(label);
},
getSummary(): { total: number; breakdown: Record<string, number> } {
const breakdown: Record<string, number> = {};
let total = 0;
for (const [label, size] of allocations) {
breakdown[label] = size;
total += size;
}
return { total, breakdown };
},
};
}Streaming Large Data
Stream Response Bodies
// ❌ Bad: Buffer entire response
async function transformResponse(response: Response): Promise<Response> {
const text = await response.text(); // Buffers all data
const transformed = transform(text);
return new Response(transformed);
}
// ✅ Good: Stream transformation
function streamTransform(response: Response): Response {
const reader = response.body?.getReader();
if (!reader) return response;
const stream = new ReadableStream({
async pull(controller) {
const { done, value } = await reader.read();
if (done) {
controller.close();
return;
}
// Transform chunk
const transformed = processChunk(value);
controller.enqueue(transformed);
},
cancel() {
reader.cancel();
},
});
return new Response(stream, {
headers: response.headers,
});
}Stream JSON Processing
// For large JSON arrays, process items one at a time
async function* streamJSONArray<T>(
response: Response
): AsyncGenerator<T, void, unknown> {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
let inArray = false;
let depth = 0;
let itemStart = -1;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
for (let i = 0; i < buffer.length; i++) {
const char = buffer[i];
if (char === '[' && !inArray) {
inArray = true;
itemStart = i + 1;
} else if (char === '{') {
if (depth === 0) itemStart = i;
depth++;
} else if (char === '}') {
depth--;
if (depth === 0 && inArray) {
const item = buffer.slice(itemStart, i + 1);
yield JSON.parse(item) as T;
itemStart = i + 2; // Skip comma
}
}
}
// Keep only unprocessed data
if (itemStart > 0) {
buffer = buffer.slice(itemStart);
itemStart = 0;
}
}
}
// Usage
for await (const item of streamJSONArray<User>(response)) {
await processUser(item);
// Each item is processed and can be GC'd
}Avoiding Memory Leaks
Clean Up Event Listeners
// ❌ Bad: Listeners accumulate
class DataProcessor {
private emitter = new EventTarget();
process(data: unknown): void {
this.emitter.addEventListener('complete', () => {
// This listener stays forever
console.log('Complete');
});
}
}
// ✅ Good: Use AbortController for cleanup
class DataProcessor {
process(data: unknown, signal?: AbortSignal): void {
const controller = new AbortController();
this.emitter.addEventListener(
'complete',
() => console.log('Complete'),
{ signal: controller.signal }
);
// Auto-cleanup when done
signal?.addEventListener('abort', () => controller.abort());
}
}Clear Caches
// Request-scoped cache with size limit
class BoundedCache<K, V> {
private cache = new Map<K, V>();
private maxSize: number;
constructor(maxSize = 100) {
this.maxSize = maxSize;
}
get(key: K): V | undefined {
return this.cache.get(key);
}
set(key: K, value: V): void {
// Evict oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear(): void {
this.cache.clear();
}
}Avoid Closures Capturing Large Data
// ❌ Bad: Closure captures largeData
function createHandler(largeData: unknown[]) {
return async (request: Request) => {
// largeData stays in memory
return Response.json(largeData);
};
}
// ✅ Good: Access data through reference
const dataStore = new Map<string, unknown[]>();
function createHandler(dataKey: string) {
return async (request: Request) => {
const data = dataStore.get(dataKey);
return Response.json(data);
};
}Efficient Data Structures
Use TypedArrays for Binary Data
// ❌ Bad: Regular array for bytes
const bytes: number[] = [];
for (let i = 0; i < 10000; i++) {
bytes.push(i % 256);
}
// ✅ Good: Uint8Array
const bytes = new Uint8Array(10000);
for (let i = 0; i < 10000; i++) {
bytes[i] = i % 256;
}Use Map/Set Over Objects for Dynamic Keys
// For frequent additions/deletions
// ❌ Less efficient with many keys
const cache: Record<string, unknown> = {};
cache[key] = value;
delete cache[key];
// ✅ More efficient for dynamic keys
const cache = new Map<string, unknown>();
cache.set(key, value);
cache.delete(key);Reuse Buffers
// ❌ Bad: Create new buffer each time
function processChunks(chunks: Uint8Array[]): Uint8Array {
const results: Uint8Array[] = [];
for (const chunk of chunks) {
const result = new Uint8Array(chunk.length);
// Process...
results.push(result);
}
return concatenate(results);
}
// ✅ Good: Reuse single buffer
function processChunks(chunks: Uint8Array[], bufferSize = 65536): Uint8Array {
const buffer = new Uint8Array(bufferSize);
const results: Uint8Array[] = [];
let offset = 0;
for (const chunk of chunks) {
// Write to buffer
buffer.set(chunk, offset);
offset += chunk.length;
// Flush when full
if (offset >= bufferSize - 1024) {
results.push(buffer.slice(0, offset));
offset = 0;
}
}
if (offset > 0) {
results.push(buffer.slice(0, offset));
}
return concatenate(results);
}Garbage Collection Hints
Null Out References
async function processLargeData(data: LargeObject): Promise<Result> {
const intermediate = transform(data);
// Release original data for GC
data = null as unknown as LargeObject;
const result = finalize(intermediate);
// Release intermediate
intermediate = null as unknown as typeof intermediate;
return result;
}Process in Chunks with GC Opportunities
async function processInChunks<T, R>(
items: T[],
processor: (batch: T[]) => Promise<R[]>,
chunkSize = 100
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
const chunkResults = await processor(chunk);
results.push(...chunkResults);
// Yield to event loop, allowing GC
await new Promise(resolve => setTimeout(resolve, 0));
}
return results;
}Memory-Efficient Patterns
Pagination for Large Results
interface PaginatedQuery {
getData(offset: number, limit: number): Promise<unknown[]>;
}
async function* paginate<T>(
query: PaginatedQuery,
pageSize = 100
): AsyncGenerator<T[], void, unknown> {
let offset = 0;
while (true) {
const page = await query.getData(offset, pageSize) as T[];
if (page.length === 0) break;
yield page;
if (page.length < pageSize) break;
offset += pageSize;
}
}
// Usage
for await (const page of paginate<User>(userQuery)) {
await processUsers(page);
// Previous page can be GC'd
}Flyweight Pattern for Repeated Objects
// Share common data between many objects
class UserFactory {
private roleCache = new Map<string, Role>();
createUser(data: UserData): User {
// Reuse role object instead of creating new one
let role = this.roleCache.get(data.roleId);
if (!role) {
role = new Role(data.roleId);
this.roleCache.set(data.roleId, role);
}
return new User(data.id, data.name, role);
}
}#!/bin/bash
# Benchmark Script for Cloudflare Workers
#
# Features:
# - Load testing with configurable concurrency
# - Latency percentile analysis
# - Cold start detection
# - Response time distribution
# - Results export (JSON/CSV)
#
# Requirements:
# - curl
# - jq (for JSON parsing)
# - bc (for calculations)
#
# Usage:
# ./benchmark.sh <url> [options]
#
# Examples:
# ./benchmark.sh https://my-worker.workers.dev/api
# ./benchmark.sh https://my-worker.workers.dev/api -n 100 -c 10
# ./benchmark.sh https://my-worker.workers.dev/api --warmup 5
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Default configuration
URL=""
REQUESTS=50
CONCURRENCY=5
WARMUP=3
TIMEOUT=30
METHOD="GET"
HEADERS=""
BODY=""
OUTPUT_FORMAT="text"
OUTPUT_FILE=""
# Logging
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# Usage
usage() {
cat << EOF
Cloudflare Workers Benchmark Tool
Usage: $0 <url> [options]
Options:
-n, --requests NUM Number of requests (default: 50)
-c, --concurrency NUM Concurrent requests (default: 5)
-w, --warmup NUM Warmup requests (default: 3)
-t, --timeout SEC Request timeout (default: 30)
-m, --method METHOD HTTP method (default: GET)
-H, --header HEADER Add header (can be repeated)
-d, --data BODY Request body for POST/PUT
-o, --output FILE Output file
-f, --format FORMAT Output format: text, json, csv (default: text)
-h, --help Show this help
Examples:
$0 https://api.example.com/health
$0 https://api.example.com/data -n 100 -c 10
$0 https://api.example.com/users -m POST -d '{"name":"test"}'
$0 https://api.example.com/api -f json -o results.json
EOF
exit 0
}
# Parse arguments
parse_args() {
if [ $# -eq 0 ]; then
usage
fi
URL="$1"
shift
while [ $# -gt 0 ]; do
case "$1" in
-n|--requests)
REQUESTS="$2"
shift 2
;;
-c|--concurrency)
CONCURRENCY="$2"
shift 2
;;
-w|--warmup)
WARMUP="$2"
shift 2
;;
-t|--timeout)
TIMEOUT="$2"
shift 2
;;
-m|--method)
METHOD="$2"
shift 2
;;
-H|--header)
HEADERS="$HEADERS -H '$2'"
shift 2
;;
-d|--data)
BODY="$2"
shift 2
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-f|--format)
OUTPUT_FORMAT="$2"
shift 2
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
;;
esac
done
if [ -z "$URL" ]; then
error "URL is required"
fi
}
# Check dependencies
check_deps() {
for cmd in curl jq bc; do
if ! command -v "$cmd" &> /dev/null; then
error "$cmd is required. Please install it."
fi
done
}
# Warmup requests
warmup() {
if [ "$WARMUP" -eq 0 ]; then
return
fi
info "Warming up with $WARMUP requests..."
for i in $(seq 1 "$WARMUP"); do
curl -s -o /dev/null -w "" \
-X "$METHOD" \
--max-time "$TIMEOUT" \
${HEADERS} \
${BODY:+-d "$BODY"} \
"$URL" &
done
wait
sleep 1
success "Warmup complete"
}
# Single request with timing
make_request() {
local result
result=$(curl -s -o /dev/null -w '%{http_code},%{time_total},%{time_connect},%{time_starttransfer}' \
-X "$METHOD" \
--max-time "$TIMEOUT" \
${HEADERS} \
${BODY:+-d "$BODY"} \
"$URL" 2>/dev/null || echo "000,0,0,0")
echo "$result"
}
# Run benchmark
run_benchmark() {
info "Running benchmark: $REQUESTS requests, $CONCURRENCY concurrent"
info "URL: $URL"
info "Method: $METHOD"
echo ""
local results_file
results_file=$(mktemp)
local completed=0
local running=0
local pids=()
# Progress tracking
show_progress() {
local percent=$((completed * 100 / REQUESTS))
printf "\r[%3d%%] Completed: %d/%d" "$percent" "$completed" "$REQUESTS"
}
# Launch requests
for i in $(seq 1 "$REQUESTS"); do
# Limit concurrency
while [ ${#pids[@]} -ge "$CONCURRENCY" ]; do
# Wait for any process to finish
for j in "${!pids[@]}"; do
if ! kill -0 "${pids[$j]}" 2>/dev/null; then
unset 'pids[j]'
((completed++)) || true
show_progress
fi
done
pids=("${pids[@]}") # Reindex array
sleep 0.01
done
# Launch request in background
(make_request >> "$results_file") &
pids+=($!)
done
# Wait for remaining
for pid in "${pids[@]}"; do
wait "$pid" 2>/dev/null || true
((completed++)) || true
show_progress
done
echo ""
echo ""
# Process results
process_results "$results_file"
rm -f "$results_file"
}
# Process and display results
process_results() {
local file="$1"
# Parse results
local total_requests=$(wc -l < "$file" | tr -d ' ')
local successful=0
local failed=0
local total_time=0
local times=()
while IFS=',' read -r status time_total time_connect time_ttfb; do
if [ "$status" = "200" ] || [ "$status" = "201" ] || [ "$status" = "204" ]; then
((successful++)) || true
# Convert to milliseconds
local ms=$(echo "$time_total * 1000" | bc)
times+=("$ms")
total_time=$(echo "$total_time + $ms" | bc)
else
((failed++)) || true
fi
done < "$file"
# Calculate statistics
local count=${#times[@]}
if [ "$count" -eq 0 ]; then
error "No successful requests"
fi
# Sort times
IFS=$'\n' sorted=($(sort -n <<<"${times[*]}"))
unset IFS
local min="${sorted[0]}"
local max="${sorted[$((count-1))]}"
local avg=$(echo "scale=2; $total_time / $count" | bc)
# Percentiles
local p50_idx=$((count * 50 / 100))
local p90_idx=$((count * 90 / 100))
local p95_idx=$((count * 95 / 100))
local p99_idx=$((count * 99 / 100))
local p50="${sorted[$p50_idx]}"
local p90="${sorted[$p90_idx]}"
local p95="${sorted[$p95_idx]}"
local p99="${sorted[$p99_idx]}"
# Standard deviation
local sum_sq=0
for t in "${times[@]}"; do
local diff=$(echo "$t - $avg" | bc)
sum_sq=$(echo "$sum_sq + $diff * $diff" | bc)
done
local stddev=$(echo "scale=2; sqrt($sum_sq / $count)" | bc)
# Requests per second
local total_sec=$(echo "scale=3; $total_time / 1000" | bc)
local rps=$(echo "scale=2; $count / $total_sec" | bc)
# Output based on format
case "$OUTPUT_FORMAT" in
json)
output_json "$total_requests" "$successful" "$failed" \
"$min" "$max" "$avg" "$stddev" \
"$p50" "$p90" "$p95" "$p99" "$rps"
;;
csv)
output_csv "$total_requests" "$successful" "$failed" \
"$min" "$max" "$avg" "$stddev" \
"$p50" "$p90" "$p95" "$p99" "$rps"
;;
*)
output_text "$total_requests" "$successful" "$failed" \
"$min" "$max" "$avg" "$stddev" \
"$p50" "$p90" "$p95" "$p99" "$rps"
;;
esac
}
# Text output
output_text() {
local total=$1 successful=$2 failed=$3
local min=$4 max=$5 avg=$6 stddev=$7
local p50=$8 p90=$9 p95=${10} p99=${11} rps=${12}
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ BENCHMARK RESULTS ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Summary:"
echo " Total Requests: $total"
echo " Successful: $successful ($(echo "scale=1; $successful * 100 / $total" | bc)%)"
echo " Failed: $failed"
echo " Requests/sec: $rps"
echo ""
echo "Latency (ms):"
echo " Min: ${min}ms"
echo " Max: ${max}ms"
echo " Avg: ${avg}ms"
echo " Std Dev: ${stddev}ms"
echo ""
echo "Percentiles:"
echo " p50 (median): ${p50}ms"
echo " p90: ${p90}ms"
echo " p95: ${p95}ms"
echo " p99: ${p99}ms"
echo ""
# Performance assessment
echo "Assessment:"
if (( $(echo "$p50 < 50" | bc -l) )); then
echo -e " ${GREEN}✓ Excellent p50 latency (< 50ms)${NC}"
elif (( $(echo "$p50 < 100" | bc -l) )); then
echo -e " ${YELLOW}○ Good p50 latency (50-100ms)${NC}"
else
echo -e " ${RED}✗ High p50 latency (> 100ms)${NC}"
fi
if (( $(echo "$p99 < 200" | bc -l) )); then
echo -e " ${GREEN}✓ Excellent p99 latency (< 200ms)${NC}"
elif (( $(echo "$p99 < 500" | bc -l) )); then
echo -e " ${YELLOW}○ Good p99 latency (200-500ms)${NC}"
else
echo -e " ${RED}✗ High p99 latency (> 500ms)${NC}"
fi
if [ -n "$OUTPUT_FILE" ]; then
# Save to file as well
{
echo "url: $URL"
echo "total_requests: $total"
echo "successful: $successful"
echo "failed: $failed"
echo "rps: $rps"
echo "min_ms: $min"
echo "max_ms: $max"
echo "avg_ms: $avg"
echo "stddev_ms: $stddev"
echo "p50_ms: $p50"
echo "p90_ms: $p90"
echo "p95_ms: $p95"
echo "p99_ms: $p99"
} > "$OUTPUT_FILE"
info "Results saved to $OUTPUT_FILE"
fi
}
# JSON output
output_json() {
local total=$1 successful=$2 failed=$3
local min=$4 max=$5 avg=$6 stddev=$7
local p50=$8 p90=$9 p95=${10} p99=${11} rps=${12}
local json=$(cat <<EOF
{
"url": "$URL",
"method": "$METHOD",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"summary": {
"total_requests": $total,
"successful": $successful,
"failed": $failed,
"requests_per_second": $rps
},
"latency_ms": {
"min": $min,
"max": $max,
"avg": $avg,
"stddev": $stddev
},
"percentiles_ms": {
"p50": $p50,
"p90": $p90,
"p95": $p95,
"p99": $p99
}
}
EOF
)
if [ -n "$OUTPUT_FILE" ]; then
echo "$json" | jq '.' > "$OUTPUT_FILE"
info "Results saved to $OUTPUT_FILE"
else
echo "$json" | jq '.'
fi
}
# CSV output
output_csv() {
local total=$1 successful=$2 failed=$3
local min=$4 max=$5 avg=$6 stddev=$7
local p50=$8 p90=$9 p95=${10} p99=${11} rps=${12}
local csv="url,method,timestamp,total,successful,failed,rps,min_ms,max_ms,avg_ms,stddev_ms,p50_ms,p90_ms,p95_ms,p99_ms"
csv+="\n$URL,$METHOD,$(date -u +%Y-%m-%dT%H:%M:%SZ),$total,$successful,$failed,$rps,$min,$max,$avg,$stddev,$p50,$p90,$p95,$p99"
if [ -n "$OUTPUT_FILE" ]; then
echo -e "$csv" > "$OUTPUT_FILE"
info "Results saved to $OUTPUT_FILE"
else
echo -e "$csv"
fi
}
# Main
main() {
parse_args "$@"
check_deps
warmup
run_benchmark
}
main "$@"
#!/bin/bash
# Worker Profiling Script
#
# Features:
# - CPU time analysis from Server-Timing headers
# - Cold start detection and tracking
# - Response time breakdown
# - Memory estimation via payload size
# - Comparative analysis (before/after)
#
# Usage:
# ./profile-worker.sh <url> [options]
#
# Examples:
# ./profile-worker.sh https://my-worker.workers.dev/api
# ./profile-worker.sh https://my-worker.workers.dev/api --iterations 20
# ./profile-worker.sh https://my-worker.workers.dev/api --compare baseline.json
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
URL=""
ITERATIONS=10
DELAY=0.5
FORCE_COLD=false
COMPARE_FILE=""
OUTPUT_FILE=""
VERBOSE=false
# Logging
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
debug() { [ "$VERBOSE" = true ] && echo -e "${CYAN}[DEBUG]${NC} $1"; }
# Usage
usage() {
cat << EOF
Worker Profiling Tool
Analyzes Cloudflare Worker performance by examining Server-Timing headers
and response characteristics.
Usage: $0 <url> [options]
Options:
-i, --iterations NUM Number of profiling iterations (default: 10)
-d, --delay SEC Delay between requests (default: 0.5)
--force-cold Wait 30s between requests to force cold starts
-c, --compare FILE Compare with baseline JSON file
-o, --output FILE Save results to JSON file
-v, --verbose Verbose output
-h, --help Show this help
Examples:
$0 https://api.example.com/profile
$0 https://api.example.com/api -i 20 -o profile.json
$0 https://api.example.com/api --force-cold
$0 https://api.example.com/api --compare old-profile.json
The worker should include Server-Timing headers:
Server-Timing: total;dur=15.5, db;dur=8.2, cache;dur=0.5
And optionally:
X-Cold-Start: true/false
X-Response-Time: 15.5ms
EOF
exit 0
}
# Parse arguments
parse_args() {
if [ $# -eq 0 ]; then
usage
fi
URL="$1"
shift
while [ $# -gt 0 ]; do
case "$1" in
-i|--iterations)
ITERATIONS="$2"
shift 2
;;
-d|--delay)
DELAY="$2"
shift 2
;;
--force-cold)
FORCE_COLD=true
shift
;;
-c|--compare)
COMPARE_FILE="$2"
shift 2
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
;;
esac
done
if [ -z "$URL" ]; then
error "URL is required"
fi
}
# Check dependencies
check_deps() {
for cmd in curl jq bc; do
if ! command -v "$cmd" &> /dev/null; then
error "$cmd is required. Please install it."
fi
done
}
# Parse Server-Timing header
parse_server_timing() {
local header="$1"
local timings="{}"
# Parse each timing entry: name;dur=value,name2;dur=value2
while IFS=',' read -ra entries; do
for entry in "${entries[@]}"; do
entry=$(echo "$entry" | xargs) # Trim whitespace
if [[ "$entry" =~ ([^;]+)\;dur=([0-9.]+) ]]; then
local name="${BASH_REMATCH[1]}"
local duration="${BASH_REMATCH[2]}"
timings=$(echo "$timings" | jq --arg n "$name" --arg d "$duration" '. + {($n): ($d | tonumber)}')
fi
done
done <<< "$header"
echo "$timings"
}
# Make profiling request
profile_request() {
local result
local headers_file=$(mktemp)
# Make request and capture headers
local body
body=$(curl -s -D "$headers_file" \
--max-time 30 \
-H "Accept: application/json" \
"$URL" 2>/dev/null)
local status=$(grep -i "HTTP/" "$headers_file" | tail -1 | awk '{print $2}')
local server_timing=$(grep -i "Server-Timing:" "$headers_file" | sed 's/[Ss]erver-[Tt]iming: //i' | tr -d '\r')
local response_time=$(grep -i "X-Response-Time:" "$headers_file" | sed 's/[Xx]-[Rr]esponse-[Tt]ime: //i' | tr -d '\r' | sed 's/ms//')
local cold_start=$(grep -i "X-Cold-Start:" "$headers_file" | sed 's/[Xx]-[Cc]old-[Ss]tart: //i' | tr -d '\r')
local content_length=$(grep -i "Content-Length:" "$headers_file" | sed 's/[Cc]ontent-[Ll]ength: //i' | tr -d '\r')
# Parse timings
local timings="{}"
if [ -n "$server_timing" ]; then
timings=$(parse_server_timing "$server_timing")
fi
# Estimate response size
local size=${content_length:-$(echo -n "$body" | wc -c)}
# Build result
local result=$(jq -n \
--arg status "$status" \
--arg response_time "${response_time:-0}" \
--arg cold "${cold_start:-false}" \
--arg size "$size" \
--argjson timings "$timings" \
'{
status: ($status | tonumber),
response_time_ms: ($response_time | tonumber),
cold_start: ($cold == "true"),
size_bytes: ($size | tonumber),
timings: $timings
}')
rm -f "$headers_file"
echo "$result"
}
# Run profiling
run_profiling() {
info "Profiling: $URL"
info "Iterations: $ITERATIONS"
echo ""
local results="[]"
local cold_count=0
local warm_count=0
for i in $(seq 1 "$ITERATIONS"); do
printf "\r[%3d%%] Iteration %d/%d" "$((i * 100 / ITERATIONS))" "$i" "$ITERATIONS"
local result=$(profile_request)
results=$(echo "$results" | jq --argjson r "$result" '. + [$r]')
# Count cold/warm
if echo "$result" | jq -e '.cold_start == true' > /dev/null; then
((cold_count++)) || true
else
((warm_count++)) || true
fi
debug "Result: $result"
# Delay between requests
if [ "$FORCE_COLD" = true ]; then
info "Waiting 30s for cold start..."
sleep 30
else
sleep "$DELAY"
fi
done
echo ""
echo ""
# Analyze results
analyze_results "$results" "$cold_count" "$warm_count"
}
# Analyze and display results
analyze_results() {
local results="$1"
local cold_count="$2"
local warm_count="$3"
# Calculate statistics
local stats=$(echo "$results" | jq '
{
count: length,
response_times: [.[].response_time_ms] | sort,
sizes: [.[].size_bytes],
cold_starts: [.[] | select(.cold_start == true)] | length,
all_timings: [.[].timings] | add
} |
{
count: .count,
cold_starts: .cold_starts,
response: {
min: (.response_times | min),
max: (.response_times | max),
avg: ((.response_times | add) / .count),
p50: .response_times[(.count / 2 | floor)],
p95: .response_times[((.count * 0.95) | floor)],
p99: .response_times[((.count * 0.99) | floor)]
},
size: {
avg: ((.sizes | add) / .count)
},
timings: (
if .all_timings then
.all_timings | to_entries | group_by(.key) | map({
key: .[0].key,
value: {
avg: ([.[].value] | add / length),
min: ([.[].value] | min),
max: ([.[].value] | max)
}
}) | from_entries
else {}
end
)
}
')
# Display results
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ PROFILING RESULTS ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Summary:"
echo " Iterations: $ITERATIONS"
echo " Cold Starts: $cold_count ($(echo "scale=1; $cold_count * 100 / $ITERATIONS" | bc)%)"
echo " Warm Requests: $warm_count ($(echo "scale=1; $warm_count * 100 / $ITERATIONS" | bc)%)"
echo ""
echo "Response Time (ms):"
echo "$stats" | jq -r '.response | " Min: \(.min)ms\n Max: \(.max)ms\n Avg: \(.avg | floor)ms\n p50: \(.p50)ms\n p95: \(.p95)ms\n p99: \(.p99)ms"'
echo ""
echo "Response Size:"
echo "$stats" | jq -r '.size | " Average: \(.avg | floor) bytes"'
echo ""
# Show individual timing breakdowns
local timing_keys=$(echo "$stats" | jq -r '.timings | keys[]' 2>/dev/null)
if [ -n "$timing_keys" ]; then
echo "Server Timing Breakdown (avg ms):"
echo "$stats" | jq -r '.timings | to_entries[] | " \(.key): \(.value.avg | floor)ms (min: \(.value.min), max: \(.value.max))"'
echo ""
fi
# Performance assessment
echo "Assessment:"
local avg_time=$(echo "$stats" | jq '.response.avg')
if (( $(echo "$avg_time < 20" | bc -l) )); then
echo -e " ${GREEN}✓ Excellent average response time (< 20ms)${NC}"
elif (( $(echo "$avg_time < 50" | bc -l) )); then
echo -e " ${GREEN}✓ Good average response time (20-50ms)${NC}"
elif (( $(echo "$avg_time < 100" | bc -l) )); then
echo -e " ${YELLOW}○ Moderate response time (50-100ms)${NC}"
else
echo -e " ${RED}✗ High response time (> 100ms)${NC}"
fi
if [ "$cold_count" -gt 0 ]; then
if (( $(echo "$cold_count > $ITERATIONS / 2" | bc -l) )); then
echo -e " ${RED}✗ High cold start rate - consider warming${NC}"
else
echo -e " ${YELLOW}○ Some cold starts detected${NC}"
fi
else
echo -e " ${GREEN}✓ No cold starts detected${NC}"
fi
# Compare with baseline
if [ -n "$COMPARE_FILE" ] && [ -f "$COMPARE_FILE" ]; then
echo ""
echo "Comparison with baseline ($COMPARE_FILE):"
compare_with_baseline "$stats"
fi
# Save output
if [ -n "$OUTPUT_FILE" ]; then
local full_results=$(jq -n \
--arg url "$URL" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson stats "$stats" \
--argjson raw "$results" \
'{
url: $url,
timestamp: $timestamp,
stats: $stats,
raw_results: $raw
}')
echo "$full_results" > "$OUTPUT_FILE"
info "Results saved to $OUTPUT_FILE"
fi
}
# Compare with baseline
compare_with_baseline() {
local current="$1"
local baseline=$(cat "$COMPARE_FILE")
local curr_avg=$(echo "$current" | jq '.response.avg')
local base_avg=$(echo "$baseline" | jq '.stats.response.avg')
local diff=$(echo "scale=2; $curr_avg - $base_avg" | bc)
local pct=$(echo "scale=1; ($diff / $base_avg) * 100" | bc)
if (( $(echo "$diff < 0" | bc -l) )); then
echo -e " ${GREEN}↓ Response time improved by ${diff#-}ms (${pct#-}% faster)${NC}"
elif (( $(echo "$diff > 0" | bc -l) )); then
echo -e " ${RED}↑ Response time degraded by ${diff}ms (${pct}% slower)${NC}"
else
echo -e " ${BLUE}= Response time unchanged${NC}"
fi
local curr_cold=$(echo "$current" | jq '.cold_starts')
local base_cold=$(echo "$baseline" | jq '.stats.cold_starts')
if [ "$curr_cold" -lt "$base_cold" ]; then
echo -e " ${GREEN}↓ Fewer cold starts ($curr_cold vs $base_cold)${NC}"
elif [ "$curr_cold" -gt "$base_cold" ]; then
echo -e " ${RED}↑ More cold starts ($curr_cold vs $base_cold)${NC}"
fi
}
# Main
main() {
parse_args "$@"
check_deps
run_profiling
}
main "$@"
/**
* Multi-Layer Caching System for Cloudflare Workers
*
* Features:
* - Memory cache (request-scoped, fastest)
* - Edge cache (Cache API, per-colo)
* - KV cache (global, persistent)
* - Stale-while-revalidate pattern
* - Cache tags for invalidation
* - TTL management
*
* Usage:
* 1. Initialize cache layers
* 2. Use get/set methods
* 3. Implement cache-aside pattern
*/
// ============================================
// TYPES
// ============================================
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
tags?: string[];
}
interface CacheLayer<T> {
get(key: string): Promise<T | null>;
set(key: string, value: T, options?: CacheOptions): Promise<void>;
delete(key: string): Promise<void>;
has(key: string): Promise<boolean>;
}
interface CacheOptions {
ttl?: number; // Time to live in seconds
tags?: string[]; // Cache tags for invalidation
staleWhileRevalidate?: number; // Serve stale for this many seconds
}
interface CacheStats {
hits: number;
misses: number;
hitRate: number;
}
// ============================================
// MEMORY CACHE (Request-Scoped)
// ============================================
export class MemoryCache<T> implements CacheLayer<T> {
private cache = new Map<string, CacheEntry<T>>();
private maxSize: number;
private stats = { hits: 0, misses: 0 };
constructor(maxSize = 100) {
this.maxSize = maxSize;
}
async get(key: string): Promise<T | null> {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return null;
}
// Check expiration
if (this.isExpired(entry)) {
this.cache.delete(key);
this.stats.misses++;
return null;
}
this.stats.hits++;
return entry.data;
}
async set(key: string, value: T, options?: CacheOptions): Promise<void> {
// Evict if at capacity (LRU-style)
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) this.cache.delete(firstKey);
}
this.cache.set(key, {
data: value,
timestamp: Date.now(),
ttl: options?.ttl ?? 60,
tags: options?.tags,
});
}
async delete(key: string): Promise<void> {
this.cache.delete(key);
}
async has(key: string): Promise<boolean> {
const entry = this.cache.get(key);
return entry !== undefined && !this.isExpired(entry);
}
getStats(): CacheStats {
const total = this.stats.hits + this.stats.misses;
return {
...this.stats,
hitRate: total > 0 ? this.stats.hits / total : 0,
};
}
clear(): void {
this.cache.clear();
}
private isExpired(entry: CacheEntry<T>): boolean {
return Date.now() > entry.timestamp + entry.ttl * 1000;
}
}
// ============================================
// EDGE CACHE (Cache API)
// ============================================
export class EdgeCache<T> implements CacheLayer<T> {
private cache: Cache;
private prefix: string;
private stats = { hits: 0, misses: 0 };
constructor(prefix = 'edge-cache:') {
this.cache = caches.default;
this.prefix = prefix;
}
private getCacheKey(key: string): Request {
return new Request(`https://cache/${this.prefix}${key}`);
}
async get(key: string): Promise<T | null> {
const response = await this.cache.match(this.getCacheKey(key));
if (!response) {
this.stats.misses++;
return null;
}
this.stats.hits++;
return response.json();
}
async set(key: string, value: T, options?: CacheOptions): Promise<void> {
const ttl = options?.ttl ?? 3600;
const response = new Response(JSON.stringify(value), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${ttl}`,
'X-Cache-Tags': options?.tags?.join(',') ?? '',
'X-Cache-Timestamp': Date.now().toString(),
},
});
await this.cache.put(this.getCacheKey(key), response);
}
async delete(key: string): Promise<void> {
await this.cache.delete(this.getCacheKey(key));
}
async has(key: string): Promise<boolean> {
const response = await this.cache.match(this.getCacheKey(key));
return response !== undefined;
}
getStats(): CacheStats {
const total = this.stats.hits + this.stats.misses;
return {
...this.stats,
hitRate: total > 0 ? this.stats.hits / total : 0,
};
}
}
// ============================================
// KV CACHE (Global)
// ============================================
export class KVCache<T> implements CacheLayer<T> {
private kv: KVNamespace;
private prefix: string;
private stats = { hits: 0, misses: 0 };
constructor(kv: KVNamespace, prefix = 'kv-cache:') {
this.kv = kv;
this.prefix = prefix;
}
async get(key: string): Promise<T | null> {
const value = await this.kv.get<CacheEntry<T>>(this.prefix + key, 'json');
if (!value) {
this.stats.misses++;
return null;
}
// Check if expired (belt and suspenders with KV expirationTtl)
if (Date.now() > value.timestamp + value.ttl * 1000) {
this.stats.misses++;
return null;
}
this.stats.hits++;
return value.data;
}
async set(key: string, value: T, options?: CacheOptions): Promise<void> {
const ttl = options?.ttl ?? 3600;
const entry: CacheEntry<T> = {
data: value,
timestamp: Date.now(),
ttl,
tags: options?.tags,
};
await this.kv.put(this.prefix + key, JSON.stringify(entry), {
expirationTtl: ttl,
});
// Store tag associations
if (options?.tags) {
for (const tag of options.tags) {
const tagKey = `tag:${tag}`;
const existingKeys = await this.kv.get<string[]>(tagKey, 'json') ?? [];
if (!existingKeys.includes(key)) {
existingKeys.push(key);
await this.kv.put(tagKey, JSON.stringify(existingKeys));
}
}
}
}
async delete(key: string): Promise<void> {
await this.kv.delete(this.prefix + key);
}
async has(key: string): Promise<boolean> {
const value = await this.kv.get(this.prefix + key);
return value !== null;
}
async invalidateByTag(tag: string): Promise<void> {
const tagKey = `tag:${tag}`;
const keys = await this.kv.get<string[]>(tagKey, 'json') ?? [];
await Promise.all([
...keys.map((key) => this.delete(key)),
this.kv.delete(tagKey),
]);
}
getStats(): CacheStats {
const total = this.stats.hits + this.stats.misses;
return {
...this.stats,
hitRate: total > 0 ? this.stats.hits / total : 0,
};
}
}
// ============================================
// MULTI-LAYER CACHE
// ============================================
export class MultiLayerCache<T> implements CacheLayer<T> {
private layers: CacheLayer<T>[];
constructor(layers: CacheLayer<T>[]) {
this.layers = layers;
}
async get(key: string): Promise<T | null> {
for (let i = 0; i < this.layers.length; i++) {
const value = await this.layers[i].get(key);
if (value !== null) {
// Populate upper layers (don't await, do in background)
this.populateUpperLayers(key, value, i);
return value;
}
}
return null;
}
async set(key: string, value: T, options?: CacheOptions): Promise<void> {
// Set in all layers
await Promise.all(
this.layers.map((layer) => layer.set(key, value, options))
);
}
async delete(key: string): Promise<void> {
await Promise.all(this.layers.map((layer) => layer.delete(key)));
}
async has(key: string): Promise<boolean> {
for (const layer of this.layers) {
if (await layer.has(key)) {
return true;
}
}
return false;
}
private async populateUpperLayers(
key: string,
value: T,
foundAtIndex: number
): Promise<void> {
// Populate all layers above where we found the value
const upperLayers = this.layers.slice(0, foundAtIndex);
await Promise.all(upperLayers.map((layer) => layer.set(key, value)));
}
}
// ============================================
// CACHE-ASIDE PATTERN
// ============================================
export class CacheAside<T> {
constructor(
private cache: CacheLayer<T>,
private defaultTTL: number = 3600
) {}
async get(
key: string,
fetcher: () => Promise<T>,
options?: CacheOptions
): Promise<T> {
// Try cache first
const cached = await this.cache.get(key);
if (cached !== null) {
return cached;
}
// Cache miss - fetch data
const data = await fetcher();
// Store in cache (don't await for faster response)
this.cache.set(key, data, {
ttl: options?.ttl ?? this.defaultTTL,
tags: options?.tags,
});
return data;
}
async invalidate(key: string): Promise<void> {
await this.cache.delete(key);
}
async refresh(
key: string,
fetcher: () => Promise<T>,
options?: CacheOptions
): Promise<T> {
const data = await fetcher();
await this.cache.set(key, data, options);
return data;
}
}
// ============================================
// STALE-WHILE-REVALIDATE
// ============================================
interface SWREntry<T> {
data: T;
timestamp: number;
maxAge: number;
staleWhileRevalidate: number;
}
export class SWRCache<T> {
private cache: CacheLayer<SWREntry<T>>;
private revalidating = new Set<string>();
constructor(cache: CacheLayer<SWREntry<T>>) {
this.cache = cache;
}
async get(
key: string,
fetcher: () => Promise<T>,
options: { maxAge: number; staleWhileRevalidate: number }
): Promise<T> {
const cached = await this.cache.get(key);
if (cached) {
const age = (Date.now() - cached.timestamp) / 1000;
// Fresh
if (age < cached.maxAge) {
return cached.data;
}
// Stale but within SWR window
if (age < cached.maxAge + cached.staleWhileRevalidate) {
// Revalidate in background
if (!this.revalidating.has(key)) {
this.revalidating.add(key);
this.revalidate(key, fetcher, options).finally(() => {
this.revalidating.delete(key);
});
}
return cached.data;
}
}
// No cache or too stale - fetch fresh
return this.revalidate(key, fetcher, options);
}
private async revalidate(
key: string,
fetcher: () => Promise<T>,
options: { maxAge: number; staleWhileRevalidate: number }
): Promise<T> {
const data = await fetcher();
await this.cache.set(key, {
data,
timestamp: Date.now(),
maxAge: options.maxAge,
staleWhileRevalidate: options.staleWhileRevalidate,
});
return data;
}
}
// ============================================
// RESPONSE CACHE (HTTP Responses)
// ============================================
export class ResponseCache {
private cache = caches.default;
async match(request: Request): Promise<Response | undefined> {
return this.cache.match(this.getCacheKey(request));
}
async put(
request: Request,
response: Response,
options?: { maxAge?: number; vary?: string[] }
): Promise<void> {
const cacheableResponse = new Response(response.body, response);
// Set cache headers
cacheableResponse.headers.set(
'Cache-Control',
`public, max-age=${options?.maxAge ?? 3600}`
);
if (options?.vary) {
cacheableResponse.headers.set('Vary', options.vary.join(', '));
}
await this.cache.put(this.getCacheKey(request), cacheableResponse);
}
async delete(request: Request): Promise<boolean> {
return this.cache.delete(this.getCacheKey(request));
}
private getCacheKey(request: Request): Request {
// Normalize cache key
const url = new URL(request.url);
url.searchParams.sort();
// Remove tracking params
['utm_source', 'utm_medium', 'utm_campaign', 'fbclid'].forEach((param) => {
url.searchParams.delete(param);
});
return new Request(url.toString(), {
method: 'GET',
});
}
}
// ============================================
// FACTORY FUNCTION
// ============================================
export function createCacheSystem<T>(
kv?: KVNamespace,
options?: { memorySize?: number; prefix?: string }
): MultiLayerCache<T> {
const layers: CacheLayer<T>[] = [
new MemoryCache<T>(options?.memorySize ?? 100),
new EdgeCache<T>(options?.prefix ?? 'cache:'),
];
if (kv) {
layers.push(new KVCache<T>(kv, options?.prefix ?? 'cache:'));
}
return new MultiLayerCache<T>(layers);
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
import { createCacheSystem, CacheAside, ResponseCache } from './caching-layer';
interface Env {
KV: KVNamespace;
}
interface User {
id: string;
name: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Option 1: Multi-layer cache
const cache = createCacheSystem<User>(env.KV);
const user = await cache.get('user:123');
// Option 2: Cache-aside pattern
const userCache = new CacheAside<User>(cache);
const userData = await userCache.get(
'user:123',
async () => {
return fetchUserFromDB('123');
},
{ ttl: 3600, tags: ['users'] }
);
// Option 3: Response caching
const responseCache = new ResponseCache();
const cached = await responseCache.match(request);
if (cached) {
return cached;
}
const response = await generateResponse(request);
await responseCache.put(request, response.clone(), { maxAge: 300 });
return response;
},
};
*/
/**
* Optimized Cloudflare Worker Template
*
* Features:
* - Performance-first architecture
* - Multi-layer caching
* - Lazy initialization
* - Streaming responses
* - Cold start optimization
* - Request coalescing
*
* Usage:
* 1. Copy as src/index.ts
* 2. Configure wrangler.jsonc
* 3. Customize handlers
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { compress } from 'hono/compress';
// ============================================
// TYPES
// ============================================
interface Env {
ENVIRONMENT: string;
KV: KVNamespace;
DB: D1Database;
ANALYTICS?: AnalyticsEngineDataset;
}
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
// ============================================
// LAZY INITIALIZATION
// ============================================
// Avoid expensive top-level operations
// Initialize on first use, reuse across requests
let dbInitialized = false;
let routeCache: Map<string, unknown> | undefined;
function getRouteCache(): Map<string, unknown> {
if (!routeCache) {
routeCache = new Map();
}
return routeCache;
}
// ============================================
// COLD START TRACKING
// ============================================
let isWarm = false;
let requestCount = 0;
function trackColdStart(): boolean {
const wasCold = !isWarm;
isWarm = true;
requestCount++;
return wasCold;
}
// ============================================
// MEMORY CACHE (Per-Isolate)
// ============================================
class IsolateCache<T> {
private cache = new Map<string, CacheEntry<T>>();
private maxSize: number;
constructor(maxSize = 50) {
this.maxSize = maxSize;
}
get(key: string): T | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
// Check TTL
if (Date.now() > entry.timestamp + entry.ttl * 1000) {
this.cache.delete(key);
return undefined;
}
return entry.data;
}
set(key: string, data: T, ttl = 60): void {
// Evict oldest if full
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey) this.cache.delete(oldestKey);
}
this.cache.set(key, { data, timestamp: Date.now(), ttl });
}
delete(key: string): void {
this.cache.delete(key);
}
}
// Singleton cache instance (persists across requests in same isolate)
const memoryCache = new IsolateCache<unknown>(100);
// ============================================
// REQUEST COALESCING
// ============================================
const inflight = new Map<string, Promise<unknown>>();
async function coalesce<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
// Check if request already in flight
const existing = inflight.get(key);
if (existing) {
return existing as Promise<T>;
}
// Start new request
const promise = fetcher().finally(() => {
inflight.delete(key);
});
inflight.set(key, promise);
return promise;
}
// ============================================
// MULTI-LAYER CACHE
// ============================================
async function getCached<T>(
key: string,
kv: KVNamespace,
fetcher: () => Promise<T>,
ttl = 300
): Promise<T> {
// Layer 1: Memory cache
const memCached = memoryCache.get(key) as T | undefined;
if (memCached !== undefined) {
return memCached;
}
// Layer 2: Edge cache
const edgeCache = caches.default;
const cacheKey = new Request(`https://cache/${key}`);
const edgeCached = await edgeCache.match(cacheKey);
if (edgeCached) {
const data = await edgeCached.json<T>();
memoryCache.set(key, data, ttl);
return data;
}
// Layer 3: KV
const kvCached = await kv.get<T>(key, 'json');
if (kvCached !== null) {
// Populate upper layers
memoryCache.set(key, kvCached, ttl);
edgeCache.put(
cacheKey,
new Response(JSON.stringify(kvCached), {
headers: { 'Cache-Control': `max-age=${ttl}` },
})
);
return kvCached;
}
// Cache miss - fetch and populate all layers
const data = await coalesce(key, fetcher);
// Don't await - populate cache in background
Promise.all([
kv.put(key, JSON.stringify(data), { expirationTtl: ttl }),
edgeCache.put(
cacheKey,
new Response(JSON.stringify(data), {
headers: { 'Cache-Control': `max-age=${ttl}` },
})
),
]);
memoryCache.set(key, data, ttl);
return data;
}
// ============================================
// STREAMING UTILITIES
// ============================================
function streamJSON<T>(
items: AsyncIterable<T>,
transform?: (item: T) => unknown
): ReadableStream {
let first = true;
return new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
controller.enqueue(encoder.encode('['));
for await (const item of items) {
if (!first) {
controller.enqueue(encoder.encode(','));
}
first = false;
const data = transform ? transform(item) : item;
controller.enqueue(encoder.encode(JSON.stringify(data)));
}
controller.enqueue(encoder.encode(']'));
controller.close();
},
});
}
// ============================================
// OPTIMIZED DATABASE QUERIES
// ============================================
async function* queryBatched<T>(
db: D1Database,
sql: string,
batchSize = 100
): AsyncGenerator<T> {
let offset = 0;
while (true) {
const { results } = await db
.prepare(`${sql} LIMIT ${batchSize} OFFSET ${offset}`)
.all<T>();
if (results.length === 0) break;
for (const row of results) {
yield row;
}
if (results.length < batchSize) break;
offset += batchSize;
}
}
// ============================================
// APP SETUP
// ============================================
const app = new Hono<{ Bindings: Env }>();
// Middleware
app.use('*', cors());
app.use('*', compress());
// Performance tracking middleware
app.use('*', async (c, next) => {
const start = performance.now();
const isCold = trackColdStart();
await next();
const duration = performance.now() - start;
// Add timing headers
c.res.headers.set('X-Response-Time', `${duration.toFixed(2)}ms`);
c.res.headers.set('X-Cold-Start', isCold.toString());
c.res.headers.set('X-Request-Count', requestCount.toString());
// Log slow requests
if (duration > 50) {
console.warn('Slow request:', {
path: c.req.path,
duration: `${duration.toFixed(2)}ms`,
cold: isCold,
});
}
});
// ============================================
// ROUTES
// ============================================
// Health check (minimal processing)
app.get('/health', (c) => {
return c.json({
status: 'healthy',
cold: !isWarm,
requests: requestCount,
});
});
// Cached data endpoint
app.get('/api/data/:key', async (c) => {
const key = c.req.param('key');
const data = await getCached(
`data:${key}`,
c.env.KV,
async () => {
// Expensive fetch - only runs on cache miss
const result = await c.env.DB
.prepare('SELECT * FROM data WHERE key = ?')
.bind(key)
.first();
return result;
},
300 // 5 min TTL
);
if (!data) {
return c.json({ error: 'Not found' }, 404);
}
return c.json(data);
});
// Streaming large dataset
app.get('/api/items', async (c) => {
const items = queryBatched<{ id: string; name: string }>(
c.env.DB,
'SELECT id, name FROM items ORDER BY created_at DESC'
);
return new Response(streamJSON(items), {
headers: {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked',
},
});
});
// Batch operations
app.post('/api/batch', async (c) => {
const { operations } = await c.req.json<{
operations: Array<{ type: string; data: unknown }>;
}>();
// Process in parallel batches
const batchSize = 10;
const results: unknown[] = [];
for (let i = 0; i < operations.length; i += batchSize) {
const batch = operations.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((op) => processOperation(c.env, op))
);
results.push(...batchResults);
}
return c.json({ results });
});
async function processOperation(
env: Env,
op: { type: string; data: unknown }
): Promise<unknown> {
// Implementation
return { success: true, type: op.type };
}
// ============================================
// ERROR HANDLING
// ============================================
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404);
});
app.onError((err, c) => {
console.error('Error:', {
message: err.message,
stack: err.stack,
path: c.req.path,
});
return c.json(
{
error: 'Internal Server Error',
message: c.env.ENVIRONMENT === 'development' ? err.message : undefined,
},
500
);
});
// ============================================
// EXPORT
// ============================================
export default {
fetch: app.fetch,
// Optional: Scheduled handler for cache warming
async scheduled(
event: ScheduledEvent,
env: Env,
ctx: ExecutionContext
): Promise<void> {
console.log('Scheduled task running:', event.cron);
// Pre-warm popular caches
ctx.waitUntil(prewarmCaches(env));
},
};
async function prewarmCaches(env: Env): Promise<void> {
const popularKeys = ['config', 'featured', 'categories'];
await Promise.all(
popularKeys.map(async (key) => {
await getCached(
`data:${key}`,
env.KV,
async () => {
return env.DB
.prepare('SELECT * FROM data WHERE key = ?')
.bind(key)
.first();
},
3600
);
})
);
}
/**
* Performance Monitoring Middleware for Cloudflare Workers
*
* Features:
* - Request timing and profiling
* - Server-Timing headers
* - Cold start detection
* - Slow request logging
* - Memory monitoring
* - Performance analytics
*
* Usage:
* 1. Import middleware functions
* 2. Wrap handlers with performance tracking
* 3. Monitor via headers or analytics
*/
// ============================================
// TYPES
// ============================================
interface Env {
ENVIRONMENT: string;
ANALYTICS?: AnalyticsEngineDataset;
}
interface TimingEntry {
name: string;
duration: number;
description?: string;
}
interface PerformanceContext {
requestId: string;
startTime: number;
timings: TimingEntry[];
isCold: boolean;
marks: Map<string, number>;
}
interface PerformanceMetrics {
requestId: string;
totalDuration: number;
isCold: boolean;
timings: Record<string, number>;
url: string;
method: string;
status: number;
colo?: string;
}
type Handler = (
request: Request,
env: Env,
ctx: ExecutionContext
) => Promise<Response>;
// ============================================
// COLD START DETECTION
// ============================================
let isWarm = false;
let isolateStartTime: number | undefined;
function detectColdStart(): boolean {
if (!isWarm) {
isWarm = true;
isolateStartTime = Date.now();
return true;
}
return false;
}
// ============================================
// PERFORMANCE CONTEXT
// ============================================
function createPerformanceContext(): PerformanceContext {
return {
requestId: crypto.randomUUID(),
startTime: performance.now(),
timings: [],
isCold: detectColdStart(),
marks: new Map(),
};
}
// ============================================
// TIMING HELPERS
// ============================================
function mark(ctx: PerformanceContext, name: string): void {
ctx.marks.set(name, performance.now());
}
function measure(
ctx: PerformanceContext,
name: string,
startMark: string,
description?: string
): number {
const start = ctx.marks.get(startMark);
if (!start) {
console.warn(`Mark "${startMark}" not found`);
return 0;
}
const duration = performance.now() - start;
ctx.timings.push({ name, duration, description });
return duration;
}
async function timeAsync<T>(
ctx: PerformanceContext,
name: string,
fn: () => Promise<T>,
description?: string
): Promise<T> {
const start = performance.now();
try {
return await fn();
} finally {
const duration = performance.now() - start;
ctx.timings.push({ name, duration, description });
}
}
function timeSync<T>(
ctx: PerformanceContext,
name: string,
fn: () => T,
description?: string
): T {
const start = performance.now();
try {
return fn();
} finally {
const duration = performance.now() - start;
ctx.timings.push({ name, duration, description });
}
}
// ============================================
// SERVER-TIMING HEADER
// ============================================
function buildServerTimingHeader(ctx: PerformanceContext): string {
const entries: string[] = [];
// Add total time
entries.push(`total;dur=${(performance.now() - ctx.startTime).toFixed(2)}`);
// Add cold start indicator
if (ctx.isCold) {
entries.push('cold;desc="Cold Start"');
}
// Add individual timings
for (const timing of ctx.timings) {
let entry = `${timing.name};dur=${timing.duration.toFixed(2)}`;
if (timing.description) {
entry += `;desc="${timing.description}"`;
}
entries.push(entry);
}
return entries.join(', ');
}
// ============================================
// PERFORMANCE MIDDLEWARE
// ============================================
export function withPerformanceTracking(handler: Handler): Handler {
return async (request, env, ctx) => {
const perfCtx = createPerformanceContext();
mark(perfCtx, 'start');
try {
const response = await timeAsync(
perfCtx,
'handler',
() => handler(request, env, ctx),
'Request handler'
);
// Add performance headers
const newResponse = new Response(response.body, response);
newResponse.headers.set('X-Request-Id', perfCtx.requestId);
newResponse.headers.set(
'X-Response-Time',
`${(performance.now() - perfCtx.startTime).toFixed(2)}ms`
);
newResponse.headers.set('Server-Timing', buildServerTimingHeader(perfCtx));
if (perfCtx.isCold) {
newResponse.headers.set('X-Cold-Start', 'true');
}
// Log metrics
logMetrics(perfCtx, request, newResponse, env);
return newResponse;
} catch (error) {
// Log error with timing
console.error('Request failed:', {
requestId: perfCtx.requestId,
duration: performance.now() - perfCtx.startTime,
error: (error as Error).message,
});
throw error;
}
};
}
// ============================================
// SLOW REQUEST DETECTION
// ============================================
const SLOW_THRESHOLD_MS = 100;
export function withSlowRequestLogging(
handler: Handler,
threshold = SLOW_THRESHOLD_MS
): Handler {
return async (request, env, ctx) => {
const start = performance.now();
try {
return await handler(request, env, ctx);
} finally {
const duration = performance.now() - start;
if (duration > threshold) {
console.warn('Slow request detected:', {
url: request.url,
method: request.method,
duration: `${duration.toFixed(2)}ms`,
threshold: `${threshold}ms`,
cf: request.cf,
});
}
}
};
}
// ============================================
// METRICS LOGGING
// ============================================
function logMetrics(
ctx: PerformanceContext,
request: Request,
response: Response,
env: Env
): void {
const metrics: PerformanceMetrics = {
requestId: ctx.requestId,
totalDuration: performance.now() - ctx.startTime,
isCold: ctx.isCold,
timings: Object.fromEntries(ctx.timings.map((t) => [t.name, t.duration])),
url: new URL(request.url).pathname,
method: request.method,
status: response.status,
colo: (request.cf as { colo?: string })?.colo,
};
// Console logging (appears in wrangler tail)
if (env.ENVIRONMENT === 'development' || ctx.isCold || metrics.totalDuration > 50) {
console.log('Performance metrics:', JSON.stringify(metrics));
}
// Analytics Engine (if available)
if (env.ANALYTICS) {
env.ANALYTICS.writeDataPoint({
blobs: [
metrics.url,
metrics.method,
ctx.isCold ? 'cold' : 'warm',
metrics.colo || 'unknown',
],
doubles: [metrics.totalDuration, metrics.status],
indexes: [metrics.requestId],
});
}
}
// ============================================
// PROFILER CLASS
// ============================================
export class RequestProfiler {
private ctx: PerformanceContext;
constructor() {
this.ctx = createPerformanceContext();
}
mark(name: string): void {
mark(this.ctx, name);
}
measure(name: string, startMark: string, description?: string): number {
return measure(this.ctx, name, startMark, description);
}
async time<T>(name: string, fn: () => Promise<T>, description?: string): Promise<T> {
return timeAsync(this.ctx, name, fn, description);
}
timeSync<T>(name: string, fn: () => T, description?: string): T {
return timeSync(this.ctx, name, fn, description);
}
getTimings(): TimingEntry[] {
return [...this.ctx.timings];
}
getTotalDuration(): number {
return performance.now() - this.ctx.startTime;
}
isColdStart(): boolean {
return this.ctx.isCold;
}
getRequestId(): string {
return this.ctx.requestId;
}
getServerTimingHeader(): string {
return buildServerTimingHeader(this.ctx);
}
getSummary(): Record<string, unknown> {
return {
requestId: this.ctx.requestId,
totalDuration: this.getTotalDuration(),
isCold: this.ctx.isCold,
timings: Object.fromEntries(this.ctx.timings.map((t) => [t.name, t.duration])),
};
}
}
// ============================================
// CPU GUARD
// ============================================
const CPU_LIMIT_MS = 30; // Paid plan limit
export function withCPUGuard(handler: Handler, limit = CPU_LIMIT_MS): Handler {
return async (request, env, ctx) => {
const start = performance.now();
let checkCount = 0;
// Periodic CPU check
const checkCPU = () => {
checkCount++;
const elapsed = performance.now() - start;
if (elapsed > limit * 0.8) {
console.warn('Approaching CPU limit:', {
elapsed: `${elapsed.toFixed(2)}ms`,
limit: `${limit}ms`,
checks: checkCount,
});
}
};
// Check every 100 iterations or time-based
const interval = setInterval(checkCPU, 10);
try {
return await handler(request, env, ctx);
} finally {
clearInterval(interval);
const total = performance.now() - start;
if (total > limit) {
console.error('CPU limit exceeded:', {
duration: `${total.toFixed(2)}ms`,
limit: `${limit}ms`,
});
}
}
};
}
// ============================================
// COMBINED MIDDLEWARE
// ============================================
export function withFullPerformanceMonitoring(handler: Handler): Handler {
// Stack middlewares
return withCPUGuard(withSlowRequestLogging(withPerformanceTracking(handler)));
}
// ============================================
// EXAMPLE USAGE
// ============================================
/*
import {
withFullPerformanceMonitoring,
RequestProfiler,
} from './performance-middleware';
// Option 1: Use middleware wrapper
export default {
fetch: withFullPerformanceMonitoring(async (request, env, ctx) => {
// Your handler code
return new Response('OK');
}),
};
// Option 2: Use profiler for granular timing
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const profiler = new RequestProfiler();
profiler.mark('start');
const user = await profiler.time('fetchUser', async () => {
return await db.getUser(userId);
});
profiler.mark('postFetch');
const data = profiler.timeSync('transform', () => {
return transformData(user);
});
profiler.measure('afterTransform', 'postFetch');
const response = Response.json(data);
response.headers.set('Server-Timing', profiler.getServerTimingHeader());
return response;
},
};
*/