edge-computing
Use this skill when deploying edge functions, writing Cloudflare Workers, configuring CDN cache logic, optimizing latency with edge-side processing, or building serverless-at-the-edge architectures. Triggers on edge functions, CDN rules, Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Lambda@Edge, cache headers, geo-routing, and any task requiring computation close to the user.
cloud edgecloudflare-workerscdnlatencyserverlessedge-functionsWhat is edge-computing?
Use this skill when deploying edge functions, writing Cloudflare Workers, configuring CDN cache logic, optimizing latency with edge-side processing, or building serverless-at-the-edge architectures. Triggers on edge functions, CDN rules, Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Lambda@Edge, cache headers, geo-routing, and any task requiring computation close to the user.
edge-computing
edge-computing is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Deploying edge functions, writing Cloudflare Workers, configuring CDN cache logic, optimizing latency with edge-side processing, or building serverless-at-the-edge architectures.
Quick Facts
| Field | Value |
|---|---|
| Category | cloud |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill edge-computing- The edge-computing skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A comprehensive skill for building, deploying, and optimizing applications that run at the network edge - close to end users rather than in centralized data centers. This covers the full edge stack: writing Cloudflare Workers and Deno Deploy functions, configuring CDN cache rules and invalidation, implementing geo-routing and A/B testing at the edge, and systematically reducing latency through edge-side processing. The core principle is to move computation to where the user is, not the other way around.
Tags
edge cloudflare-workers cdn latency serverless edge-functions
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair edge-computing with these complementary skills:
Frequently Asked Questions
What is edge-computing?
Use this skill when deploying edge functions, writing Cloudflare Workers, configuring CDN cache logic, optimizing latency with edge-side processing, or building serverless-at-the-edge architectures. Triggers on edge functions, CDN rules, Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Lambda@Edge, cache headers, geo-routing, and any task requiring computation close to the user.
How do I install edge-computing?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill edge-computing in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support edge-computing?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Edge Computing
A comprehensive skill for building, deploying, and optimizing applications that run at the network edge - close to end users rather than in centralized data centers. This covers the full edge stack: writing Cloudflare Workers and Deno Deploy functions, configuring CDN cache rules and invalidation, implementing geo-routing and A/B testing at the edge, and systematically reducing latency through edge-side processing. The core principle is to move computation to where the user is, not the other way around.
When to use this skill
Trigger this skill when the user:
- Wants to write or debug a Cloudflare Worker, Deno Deploy function, or Vercel Edge Function
- Needs to configure CDN cache headers, cache keys, or invalidation strategies
- Is implementing geo-routing, A/B testing, or feature flags at the edge
- Wants to reduce TTFB or latency by moving logic closer to users
- Needs to transform requests or responses at the CDN layer
- Is working with edge-side KV stores, Durable Objects, or D1 databases
- Wants to implement authentication, rate limiting, or bot protection at the edge
- Is debugging cold start times or execution limits in edge runtimes
Do NOT trigger this skill for:
- General serverless architecture with traditional Lambda/Cloud Functions (use cloud-aws or cloud-gcp skill)
- Full backend API design that belongs in a centralized server (use backend-engineering skill)
Key principles
Edge is not a server - respect the constraints - Edge runtimes use V8 isolates, not Node.js. No filesystem access, limited CPU time (typically 10-50ms for free tiers), restricted APIs (no
eval, no native modules). Design for these constraints from the start rather than porting server code and hoping it works.Cache aggressively, invalidate precisely - The fastest request is one that never reaches your origin. Set long
Cache-Controlmax-age on immutable assets, usestale-while-revalidatefor dynamic content, and implement surgical cache purging by surrogate key or tag rather than full-site flushes.Minimize origin round-trips - Every request back to origin adds 50-200ms of latency. Use edge KV stores for read-heavy data, coalesce multiple origin fetches with Promise.all, and implement request collapsing so concurrent identical requests share a single origin fetch.
Fail open, not closed - When the edge function errors or times out, fall through to the origin server rather than showing an error page. Edge logic should enhance performance, not become a single point of failure.
Measure from the user's perspective - TTFB measured from your data center is meaningless. Use Real User Monitoring (RUM) with geographic breakdowns to understand actual latency. Synthetic tests from a single region miss the whole point of edge.
Core concepts
V8 isolates vs containers - Edge platforms like Cloudflare Workers use V8 isolates instead of containers. An isolate starts in under 5ms (vs 50-500ms for a cold container), shares a single process with other isolates, and has hard memory limits (~128MB). This architecture enables near-zero cold starts but restricts you to Web Platform APIs only.
Edge locations and PoPs - A Point of Presence (PoP) is a physical data center in the CDN network. Cloudflare has 300+ PoPs, AWS CloudFront has 400+. Your edge code runs at whichever PoP is geographically closest to the requesting user. Understanding PoP distribution matters for cache hit ratios - more PoPs means more cache fragmentation.
Cache tiers - Most CDNs use a tiered caching architecture: L1 (edge PoP closest to user) -> L2 (regional shield/tier) -> Origin. The L2 tier reduces origin load by coalescing requests from multiple L1 PoPs. Configure cache tiers explicitly when available (Cloudflare Tiered Cache, CloudFront Origin Shield).
Edge KV and state - Edge is inherently stateless per-request, but platforms provide persistence layers: Cloudflare KV (eventually consistent, read-optimized), Durable Objects (strongly consistent, single-point coordination), D1 (SQLite at the edge), and R2 (S3-compatible object storage). Choose based on consistency requirements and read/write ratio.
Request lifecycle at the edge - Incoming request -> DNS resolution -> nearest PoP -> edge function executes -> checks cache -> (cache miss) fetches from origin -> transforms response -> caches result -> returns to client. Understanding this flow is essential for placing logic at the right phase.
Common tasks
Write a Cloudflare Worker
Basic request/response handler using the Workers API:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// Route handling
if (url.pathname === '/api/health') {
return new Response('OK', { status: 200 });
}
// Fetch from origin and transform
const response = await fetch(request);
const html = await response.text();
const modified = html.replace('</head>', '<script src="/analytics.js"></script></head>');
return new Response(modified, {
status: response.status,
headers: response.headers,
});
},
};Workers have a 10ms CPU time limit on the free plan (50ms on paid). Use
ctx.waitUntil()for non-blocking async work like logging that should not block the response.
Configure cache headers for optimal CDN behavior
Set cache-control headers that balance freshness with performance:
function setCacheHeaders(response: Response, type: 'static' | 'dynamic' | 'api'): Response {
const headers = new Headers(response.headers);
switch (type) {
case 'static':
// Immutable assets with content hash in filename
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
break;
case 'dynamic':
// HTML pages - serve stale while revalidating in background
headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=86400');
headers.set('Surrogate-Key', 'page-content');
break;
case 'api':
// API responses - short cache with revalidation
headers.set('Cache-Control', 'public, max-age=5, stale-while-revalidate=30');
headers.set('Vary', 'Authorization, Accept');
break;
}
return new Response(response.body, { status: response.status, headers });
}Always set
Varyheaders for responses that change based on request headers (e.g.,Accept-Encoding,Authorization). Missing Vary headers cause cache poisoning where one user gets another's personalized response.
Implement geo-routing at the edge
Route users to region-specific content or origins based on their location:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const country = request.headers.get('CF-IPCountry') ?? 'US';
const continent = request.cf?.continent ?? 'NA';
// Route to nearest regional origin
const origins: Record<string, string> = {
EU: 'https://eu.api.example.com',
AS: 'https://ap.api.example.com',
NA: 'https://us.api.example.com',
};
const origin = origins[continent] ?? origins['NA'];
// GDPR compliance - block or redirect EU users to compliant flow
if (continent === 'EU' && new URL(request.url).pathname.startsWith('/track')) {
return new Response('Tracking disabled in EU', { status: 451 });
}
const url = new URL(request.url);
url.hostname = new URL(origin).hostname;
return fetch(url.toString(), request);
},
};Use edge KV for read-heavy data
Store configuration, feature flags, or lookup tables in Cloudflare KV:
interface Env {
CONFIG_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Read feature flags from KV (eventually consistent, ~60s propagation)
const flags = await env.CONFIG_KV.get('feature-flags', 'json') as Record<string, boolean> | null;
if (flags?.['maintenance-mode']) {
return new Response('We are performing maintenance. Back soon.', {
status: 503,
headers: { 'Retry-After': '300' },
});
}
// Cache KV reads in the Worker's memory for the request lifetime
// KV reads are fast (~10ms) but not free - avoid reading per-subrequest
const config = await env.CONFIG_KV.get('site-config', 'json');
return fetch(request);
},
};KV is eventually consistent with ~60 second propagation. Do not use it for data that requires strong consistency (use Durable Objects instead).
Implement rate limiting at the edge
Block abusive traffic before it reaches your origin:
interface Env {
RATE_LIMITER: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown';
const key = `${ip}:${new URL(request.url).pathname}`;
// Use Durable Object for consistent rate counting
const id = env.RATE_LIMITER.idFromName(key);
const limiter = env.RATE_LIMITER.get(id);
const allowed = await limiter.fetch('https://internal/check');
if (!allowed.ok) {
return new Response('Rate limit exceeded', {
status: 429,
headers: { 'Retry-After': '60' },
});
}
return fetch(request);
},
};Perform A/B testing at the edge
Split traffic without client-side JavaScript or origin involvement:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Sticky assignment via cookie
let variant = getCookie(request, 'ab-variant');
if (!variant) {
variant = Math.random() < 0.5 ? 'control' : 'experiment';
}
// Rewrite to variant-specific origin path
if (variant === 'experiment' && url.pathname === '/pricing') {
url.pathname = '/pricing-v2';
}
const response = await fetch(url.toString(), request);
const newResponse = new Response(response.body, response);
// Set sticky cookie so user stays in same variant
newResponse.headers.append('Set-Cookie', `ab-variant=${variant}; Path=/; Max-Age=86400`);
// Vary on cookie to prevent cache mixing variants
newResponse.headers.set('Vary', 'Cookie');
return newResponse;
},
};
function getCookie(request: Request, name: string): string | null {
const cookies = request.headers.get('Cookie') ?? '';
const match = cookies.match(new RegExp(`${name}=([^;]+)`));
return match ? match[1] : null;
}Optimize cold starts and execution time
Minimize startup cost and stay within CPU limits:
// Hoist expensive initialization outside the fetch handler
// This runs once per isolate, not per request
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const STATIC_CONFIG = { version: '1.0', maxRetries: 3 };
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const start = Date.now();
// Use streaming to reduce memory pressure and TTFB
const originResponse = await fetch('https://api.example.com/data');
const { readable, writable } = new TransformStream();
// Non-blocking: pipe transform in background
ctx.waitUntil(transformStream(originResponse.body!, writable));
// Log timing without blocking response
ctx.waitUntil(
Promise.resolve().then(() => {
console.log(`Request processed in ${Date.now() - start}ms`);
})
);
return new Response(readable, {
headers: { 'Content-Type': 'application/json' },
});
},
};
async function transformStream(input: ReadableStream, output: WritableStream): Promise<void> {
const reader = input.getReader();
const writer = output.getWriter();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writer.write(value);
}
} finally {
await writer.close();
}
}Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Using Node.js APIs in edge functions | Edge runtimes are V8 isolates, not Node.js - fs, path, Buffer global are unavailable |
Use Web Platform APIs: fetch, Request, Response, TextEncoder, crypto.subtle |
| Caching personalized responses without Vary | User A sees User B's dashboard; cache poisoning at scale | Always set Vary: Cookie or Vary: Authorization on personalized responses |
| Storing mutable state in KV for counters | KV is eventually consistent - concurrent increments lose writes silently | Use Durable Objects for counters, locks, and any read-modify-write patterns |
| Catching all errors silently at the edge | Origin never sees the request; debugging becomes impossible | Fail open - on error, pass request through to origin and log the error via ctx.waitUntil |
| Putting entire app logic in a single Worker | Hits CPU time limits; becomes unmaintainable; defeats the purpose of edge (simple, fast) | Keep edge logic thin: routing, caching, auth checks, transforms. Heavy logic stays at origin |
| Ignoring cache key design | Default cache keys cause low hit rates for URLs with query params or headers | Explicitly define cache keys to strip unnecessary query params and normalize URLs |
Gotchas
ctx.waitUntil()is required for async work afterResponseis returned - Anyawaitafter you return aResponsein a Cloudflare Worker is silently dropped. Logging, analytics calls, and cache writes that happen post-response must be wrapped inctx.waitUntil(promise)or they never execute.Cloudflare KV has ~60 second eventual consistency - don't use it for flags that must take effect immediately - A KV write to disable a feature or block a user may take up to a minute to propagate across all PoPs. If you need instant effect (rate limiting, auth revocation), use Durable Objects, not KV.
Vary: Cookieon cached responses causes catastrophic cache fragmentation - SettingVary: Cookietells CDNs to cache a separate copy for every unique Cookie header value. Most users have unique session cookies, effectively making your cache useless. Instead, strip the cookie from the cache key and use a separateVaryvalue that identifies the variant (e.g., a normalized A/B bucket cookie).Edge functions can't use Node.js built-ins even if they're in
node_modules- A library that usesrequire('crypto'),require('buffer'), orrequire('path')will fail at runtime in a V8 isolate even though the import succeeds at build time. Audit all dependencies for Node.js API usage before deploying to edge.A/B test cookie without
Vary: Cookieon the response causes cache mixing - If you set anab-variantcookie but don't setVary: Cookie(or a more targeted Vary), CDN caches may serve one variant's cached response to users assigned the other variant. Always pair sticky cookies with appropriate Vary headers.
References
Load the relevant reference file only when the current task requires it:
references/cloudflare-workers.md- Cloudflare Workers API reference, wrangler CLI, bindings (KV, R2, D1, Durable Objects), and deployment patternsreferences/cdn-caching.md- Cache-Control directives, surrogate keys, cache tiers, invalidation strategies, and CDN-specific headers across providersreferences/latency-optimization.md- TTFB reduction techniques, connection reuse, edge-side includes, streaming responses, and RUM measurement
References
cdn-caching.md
CDN Caching Reference
Cache-Control directives
Response directives (server to CDN/browser)
| Directive | Effect | Example |
|---|---|---|
public |
Any cache (CDN, browser) may store | public, max-age=3600 |
private |
Only browser cache, not CDN | private, max-age=600 |
no-cache |
Cache may store but must revalidate every time | no-cache |
no-store |
Do not cache at all | no-store |
max-age=N |
Fresh for N seconds from origin response | max-age=86400 |
s-maxage=N |
Fresh for N seconds in shared caches (CDN) only | s-maxage=3600 |
stale-while-revalidate=N |
Serve stale for N seconds while fetching fresh copy in background | max-age=60, stale-while-revalidate=86400 |
stale-if-error=N |
Serve stale for N seconds if origin returns 5xx | max-age=60, stale-if-error=3600 |
immutable |
Never revalidate (use with content-hashed URLs) | max-age=31536000, immutable |
must-revalidate |
Once stale, must revalidate before use | max-age=3600, must-revalidate |
no-transform |
CDN must not modify body (no image optimization, compression changes) | no-transform |
Common patterns
# Static assets with content hash (app.a1b2c3.js)
Cache-Control: public, max-age=31536000, immutable
# HTML pages - always fresh but fast
Cache-Control: public, max-age=0, must-revalidate
# or with stale-while-revalidate for speed
Cache-Control: public, max-age=60, stale-while-revalidate=86400
# API responses - short TTL
Cache-Control: public, s-maxage=10, stale-while-revalidate=30
# Personalized content - browser only
Cache-Control: private, max-age=300
# Sensitive data - no caching anywhere
Cache-Control: no-storeVary header
The Vary header tells caches to store separate versions based on request header values.
# Different response per encoding
Vary: Accept-Encoding
# Different response per auth token (effectively uncacheable at CDN)
Vary: Authorization
# Multiple varies
Vary: Accept-Encoding, Accept-Language, CookieCritical rule: If your response depends on cookies or auth headers, you MUST
set Vary appropriately or risk serving one user's content to another.
Performance impact: Each unique combination of Vary header values creates a
separate cache entry. Vary: Cookie effectively disables CDN caching because
cookies differ per user. Prefer Vary on specific, low-cardinality headers.
Surrogate keys / cache tags
Surrogate keys allow targeted cache invalidation without purging entire zones.
Cloudflare (Enterprise)
# Response header
Surrogate-Key: product-123 category-shoes homepage
# Purge by tag via API
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {token}" \
-d '{"tags": ["product-123"]}'Fastly
# Response header
Surrogate-Key: product-123 category-shoes
# Purge
curl -X POST "https://api.fastly.com/service/{id}/purge/product-123" \
-H "Fastly-Key: {token}"CloudFront (invalidation paths)
# CloudFront uses path-based invalidation, not tags
aws cloudfront create-invalidation \
--distribution-id E1234 \
--paths "/products/123" "/category/shoes/*"CloudFront invalidations are slow (5-15 minutes) and limited to 1000 free/month. For frequent invalidation, use versioned URLs instead.
Cache key design
The cache key determines what makes a request "unique" for caching purposes.
Default cache key components
Most CDNs use: scheme + host + path + query string as the default cache key.
Custom cache key strategies
// Cloudflare Workers - normalize cache key
function getCacheKey(request: Request): Request {
const url = new URL(request.url);
// Strip tracking query params that don't affect content
const stripParams = ['utm_source', 'utm_medium', 'utm_campaign', 'fbclid', 'gclid'];
stripParams.forEach((p) => url.searchParams.delete(p));
// Sort remaining params for consistency
url.searchParams.sort();
// Normalize to lowercase path
url.pathname = url.pathname.toLowerCase();
return new Request(url.toString(), request);
}Common cache key mistakes
| Mistake | Effect | Fix |
|---|---|---|
| Not stripping UTM params | Same page cached N times for N campaign links | Strip marketing params from cache key |
| Including session cookies in cache key | Every user gets a unique cache entry (0% hit rate) | Exclude session cookies; use Vary only for meaningful headers |
| Case-sensitive paths | /Products/ and /products/ cached separately |
Normalize to lowercase |
| Random query param order | ?a=1&b=2 and ?b=2&a=1 cached separately |
Sort query params |
Tiered caching
How tiers work
User -> L1 Edge PoP (closest) -> L2 Regional Shield -> Origin
L1 cache miss: check L2 before going to origin
L2 cache miss: fetch from origin, populate both L2 and L1Configuration
Cloudflare Tiered Cache: Enable in dashboard or via API. Argo Tiered Cache uses Cloudflare's network to route cache misses through optimal regional PoPs.
CloudFront Origin Shield: Enable per-origin, choose the region closest to your origin server. Costs $0.0090/10,000 requests.
Fastly Shielding: Configure a shield PoP per backend in VCL or UI.
Benefits
- Reduces origin load by 50-90% (L1 misses are absorbed by L2)
- Improves cache hit ratio because L2 aggregates requests from many L1 PoPs
- Fewer origin connections means lower origin cost
Cache invalidation strategies
| Strategy | When to use | Latency | Complexity |
|---|---|---|---|
| TTL expiry | Content changes on predictable schedule | Seconds to hours | Low |
| Surrogate key purge | Content updated on-demand (CMS publish, product update) | 1-5 seconds | Medium |
| Versioned URLs | Static assets (CSS, JS, images) | Instant (new URL = new cache) | Low |
| Soft purge (stale-while-revalidate) | Need instant updates without origin spikes | Immediate (serves stale briefly) | Medium |
| Full zone purge | Emergency only - nuclear option | 30-60 seconds | Low |
Best practice: combine strategies
Static assets: versioned URLs (app.abc123.js) + immutable cache
HTML pages: short TTL (60s) + stale-while-revalidate (24h) + surrogate key purge on publish
API responses: short TTL (5-10s) + stale-while-revalidate (30s)
User data: private, no-store, or short max-age with Vary: AuthorizationETag and conditional requests
# Origin response
ETag: "abc123"
Cache-Control: public, max-age=60
# After TTL expires, CDN sends conditional request
If-None-Match: "abc123"
# Origin responds 304 Not Modified (no body transferred)
# CDN refreshes TTL and serves cached bodyETags reduce bandwidth but still require an origin round-trip on revalidation.
For truly static content, prefer long max-age with versioned URLs to avoid
revalidation entirely.
CDN-specific headers
| Header | CDN | Purpose |
|---|---|---|
CF-Cache-Status |
Cloudflare | HIT, MISS, EXPIRED, DYNAMIC, BYPASS |
X-Cache |
CloudFront, Fastly | Hit from cloudfront, Miss from cloudfront |
Age |
All | Seconds since object was cached |
CDN-Cache-Control |
Cloudflare | Override Cache-Control for CDN only (browser ignores) |
Surrogate-Control |
Fastly | CDN-only cache directives (stripped before browser) |
X-Cache-Hits |
Fastly | Number of times this object has been served from cache |
cloudflare-workers.md
Cloudflare Workers Reference
Runtime environment
Workers run on V8 isolates with Web Platform APIs. Key differences from Node.js:
- No
require()or CommonJS - use ES modules (import/export) - No
process,Bufferglobal,fs,path- useTextEncoder,crypto.subtle, Web Streams - CPU time limit: 10ms (free), 50ms (paid), measured as actual CPU, not wall-clock
- Memory limit: 128MB per isolate
- Max request body: 100MB (free plan restricted further)
- Subrequest limit: 50 fetch calls per request (1000 on paid)
Module format
// The standard Workers module format (recommended)
export interface Env {
MY_KV: KVNamespace;
MY_R2: R2Bucket;
MY_DB: D1Database;
MY_DO: DurableObjectNamespace;
MY_SECRET: string; // from wrangler secret
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// handle request
return new Response('Hello');
},
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
// cron trigger handler
ctx.waitUntil(doWork(env));
},
async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void> {
// queue consumer handler
for (const msg of batch.messages) {
console.log(msg.body);
msg.ack();
}
},
};Wrangler CLI
# Create new project
npx wrangler init my-worker
# Dev server with live reload
npx wrangler dev
# Deploy to production
npx wrangler deploy
# Tail production logs
npx wrangler tail
# Manage secrets
npx wrangler secret put MY_API_KEY
npx wrangler secret list
# KV namespace management
npx wrangler kv:namespace create MY_KV
npx wrangler kv:key put --namespace-id=<id> "key" "value"
npx wrangler kv:key get --namespace-id=<id> "key"
# D1 database
npx wrangler d1 create my-database
npx wrangler d1 execute my-database --command "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"wrangler.toml configuration
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# KV binding
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
preview_id = "def456"
# R2 bucket
[[r2_buckets]]
binding = "MY_R2"
bucket_name = "my-bucket"
# D1 database
[[d1_databases]]
binding = "MY_DB"
database_name = "my-database"
database_id = "xyz789"
# Durable Object
[durable_objects]
bindings = [
{ name = "MY_DO", class_name = "MyDurableObject" }
]
[[migrations]]
tag = "v1"
new_classes = ["MyDurableObject"]
# Cron triggers
[triggers]
crons = ["*/5 * * * *"] # every 5 minutes
# Environment-specific overrides
[env.staging]
name = "my-worker-staging"
routes = [{ pattern = "staging.example.com/*", zone_name = "example.com" }]
[env.production]
name = "my-worker-production"
routes = [{ pattern = "example.com/*", zone_name = "example.com" }]KV Namespace API
// Write
await env.MY_KV.put('key', 'value');
await env.MY_KV.put('key', JSON.stringify(data), {
expirationTtl: 3600, // seconds until auto-delete
metadata: { version: 1 }, // small metadata object (max 1024 bytes)
});
// Read
const value = await env.MY_KV.get('key'); // string | null
const data = await env.MY_KV.get('key', 'json'); // parsed JSON
const binary = await env.MY_KV.get('key', 'arrayBuffer'); // ArrayBuffer
const withMeta = await env.MY_KV.getWithMetadata('key', 'json');
// { value: T | null, metadata: M | null }
// Delete
await env.MY_KV.delete('key');
// List keys (paginated, max 1000 per call)
const list = await env.MY_KV.list({ prefix: 'user:', limit: 100 });
// { keys: Array<{ name: string, expiration?: number, metadata?: unknown }>, list_complete: boolean, cursor?: string }Durable Objects
export class RateLimiter {
private state: DurableObjectState;
private requests: number[] = [];
constructor(state: DurableObjectState, env: Env) {
this.state = state;
// Load persisted state on construction
this.state.blockConcurrencyWhile(async () => {
this.requests = (await this.state.storage.get<number[]>('requests')) ?? [];
});
}
async fetch(request: Request): Promise<Response> {
const now = Date.now();
const windowMs = 60_000;
const maxRequests = 100;
// Prune old entries
this.requests = this.requests.filter((t) => now - t < windowMs);
if (this.requests.length >= maxRequests) {
return new Response('Rate limited', { status: 429 });
}
this.requests.push(now);
await this.state.storage.put('requests', this.requests);
return new Response('OK', { status: 200 });
}
}Key Durable Object properties:
- Single-threaded - only one instance of a named DO exists globally
- Strong consistency - reads and writes within a DO are serialized
- Location-aware - the DO runs near its first requester, then stays put
- Storage API -
state.storage.get/put/delete/listwith transactional semantics - Hibernation - idle DOs are evicted from memory but state persists
D1 (SQLite at the edge)
// Query
const result = await env.MY_DB.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first<User>();
// Batch queries (single round-trip)
const results = await env.MY_DB.batch([
env.MY_DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice'),
env.MY_DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Bob'),
]);
// Raw query for dynamic SQL
const { results: rows } = await env.MY_DB.prepare(
'SELECT * FROM users WHERE name LIKE ? LIMIT ?'
).bind('%search%', 10).all<User>();Cache API
// Use the Workers Cache API for fine-grained cache control
const cache = caches.default;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const cacheKey = new Request(request.url, request);
// Check cache first
let response = await cache.match(cacheKey);
if (response) return response;
// Fetch from origin
response = await fetch(request);
// Clone and cache (response body can only be read once)
const responseToCache = new Response(response.body, response);
responseToCache.headers.set('Cache-Control', 'public, max-age=3600');
// waitUntil so caching doesn't block response
ctx.waitUntil(cache.put(cacheKey, responseToCache.clone()));
return responseToCache;
},
};Other edge platforms comparison
| Feature | Cloudflare Workers | Vercel Edge Functions | Deno Deploy | Lambda@Edge |
|---|---|---|---|---|
| Runtime | V8 isolate | V8 isolate (Node subset) | Deno (V8) | Node.js container |
| Cold start | <5ms | <5ms | <10ms | 50-500ms |
| CPU limit | 10-50ms | 25ms (Hobby) | 50ms | 5-30s |
| KV store | Workers KV | Vercel KV (Redis) | Deno KV | DynamoDB |
| Deploy | wrangler deploy | git push | deployctl | SAM/CDK |
| Locations | 300+ PoPs | ~20 regions | 35+ regions | CloudFront PoPs |
latency-optimization.md
Latency Optimization Reference
Understanding latency sources
Every HTTP request accumulates latency from multiple sources:
DNS lookup: 1-50ms (cached: ~0ms)
TCP handshake: 10-100ms (one RTT)
TLS handshake: 20-200ms (one or two RTTs)
TTFB (server): 10-500ms (processing time + network)
Content transfer: varies (depends on size and bandwidth)Total cold request: 50-850ms before first byte of content arrives. Warm/cached request: 0-10ms if served from edge cache.
The goal of edge computing is to eliminate as many of these layers as possible by serving responses from the nearest PoP.
TTFB reduction techniques
1. Serve from edge cache (eliminate origin)
The single biggest TTFB win. If the response is in edge cache, the entire origin round-trip (50-200ms) is eliminated.
// Cloudflare Worker: aggressive caching with background revalidation
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const cache = caches.default;
const cacheKey = request;
let response = await cache.match(cacheKey);
if (response) {
// Check if stale - if so, revalidate in background
const age = parseInt(response.headers.get('Age') ?? '0');
const maxAge = 60; // seconds
if (age > maxAge) {
ctx.waitUntil(revalidateAndCache(request, cache, cacheKey));
}
return response;
}
// Cache miss - fetch and cache
response = await fetch(request);
const cachedResponse = new Response(response.body, response);
cachedResponse.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=86400');
ctx.waitUntil(cache.put(cacheKey, cachedResponse.clone()));
return cachedResponse;
},
};
async function revalidateAndCache(
request: Request,
cache: Cache,
cacheKey: Request
): Promise<void> {
const fresh = await fetch(request);
const toCache = new Response(fresh.body, fresh);
toCache.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=86400');
await cache.put(cacheKey, toCache);
}2. Connection reuse and keep-alive
Reusing TCP/TLS connections to origin eliminates handshake latency (30-300ms per new connection).
// Cloudflare Workers automatically reuse connections to origin
// But ensure your origin supports keep-alive:
// - Set Connection: keep-alive (HTTP/1.1 default)
// - Enable HTTP/2 on your origin for multiplexing
// - Set reasonable keep-alive timeout (60-120s)3. Early hints (103)
Send the browser a 103 Early Hints response before the full response is ready, allowing it to preload critical resources while the server processes the request.
// Cloudflare supports Early Hints via Link headers
export default {
async fetch(request: Request): Promise<Response> {
const response = await fetch(request);
const newResponse = new Response(response.body, response);
// These Link headers are sent as 103 Early Hints by Cloudflare
newResponse.headers.append('Link', '</styles/main.css>; rel=preload; as=style');
newResponse.headers.append('Link', '</scripts/app.js>; rel=preload; as=script');
newResponse.headers.append('Link', '</fonts/inter.woff2>; rel=preload; as=font; crossorigin');
return newResponse;
},
};4. Request collapsing / coalescing
When multiple users request the same uncached resource simultaneously, only send one request to origin and share the response.
Cloudflare does this automatically for cacheable content. For Workers:
// Manual request coalescing with in-flight map
const inFlight = new Map<string, Promise<Response>>();
async function fetchWithCoalescing(url: string): Promise<Response> {
const existing = inFlight.get(url);
if (existing) return existing.then((r) => r.clone());
const promise = fetch(url).then((r) => {
inFlight.delete(url);
return r;
});
inFlight.set(url, promise);
return promise.then((r) => r.clone());
}Note: In Workers, the in-flight map only persists within a single isolate instance. Cross-isolate coalescing requires Durable Objects or CDN-level features.
5. Streaming responses
Start sending bytes to the client before the full response is assembled. Reduces perceived TTFB because the browser can start parsing HTML/rendering while the rest streams in.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
// Start streaming immediately
const streamBody = async () => {
// Send HTML head immediately (browser starts loading CSS/JS)
await writer.write(encoder.encode('<!DOCTYPE html><html><head><link rel="stylesheet" href="/style.css"></head><body>'));
// Fetch data (this takes time)
const data = await fetch('https://api.example.com/data').then((r) => r.json());
// Stream the body content
await writer.write(encoder.encode(`<main>${renderContent(data)}</main>`));
await writer.write(encoder.encode('</body></html>'));
await writer.close();
};
// Don't await - let it stream
streamBody();
return new Response(readable, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
},
};6. Prefetching and preconnecting
Instruct the browser to start connections early:
<!-- DNS prefetch for third-party domains -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- Preconnect: DNS + TCP + TLS (saves 100-300ms) -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<!-- Prefetch: download resource for likely next navigation -->
<link rel="prefetch" href="/next-page.html">
<!-- Preload: download critical resource for current page -->
<link rel="preload" href="/hero-image.webp" as="image">Edge-side includes (ESI)
Compose pages from cached fragments with different TTLs:
<!-- Main page template cached for 24 hours -->
<html>
<body>
<header>
<!-- User-specific nav cached for 5 minutes -->
<esi:include src="/fragments/nav?user=123" />
</header>
<main>
<!-- Product content cached for 1 hour -->
<esi:include src="/fragments/product/456" />
</main>
<footer>
<!-- Static footer cached indefinitely -->
<esi:include src="/fragments/footer" />
</footer>
</body>
</html>Workers equivalent (no ESI needed):
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Fetch fragments in parallel with different cache policies
const [nav, product, footer] = await Promise.all([
fetchCached('/fragments/nav', 300), // 5 min
fetchCached('/fragments/product/456', 3600), // 1 hour
fetchCached('/fragments/footer', 86400), // 24 hours
]);
const html = `<html><body>
<header>${nav}</header>
<main>${product}</main>
<footer>${footer}</footer>
</body></html>`;
return new Response(html, {
headers: { 'Content-Type': 'text/html' },
});
},
};
async function fetchCached(path: string, ttl: number): Promise<string> {
const response = await fetch(`https://origin.example.com${path}`, {
cf: { cacheTtl: ttl, cacheEverything: true },
});
return response.text();
}Measuring latency correctly
Real User Monitoring (RUM)
// Collect Navigation Timing data from real users
function collectTimings(): void {
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
const metrics = {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
tls: nav.secureConnectionStart > 0 ? nav.connectEnd - nav.secureConnectionStart : 0,
ttfb: nav.responseStart - nav.requestStart,
download: nav.responseEnd - nav.responseStart,
domReady: nav.domContentLoadedEventEnd - nav.fetchStart,
fullLoad: nav.loadEventEnd - nav.fetchStart,
};
// Send to analytics endpoint
navigator.sendBeacon('/analytics/timing', JSON.stringify(metrics));
}
// Run after page load
window.addEventListener('load', () => setTimeout(collectTimings, 0));Geographic breakdown
Always segment latency data by geography. A global P50 of 150ms might hide:
- US users at 50ms
- EU users at 120ms
- Southeast Asia users at 400ms (no nearby PoP, or origin in US-East)
Synthetic monitoring
# Test from multiple regions with curl
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" https://example.com
# Use tools like:
# - Catchpoint / Pingdom for multi-region synthetic checks
# - WebPageTest for full waterfall analysis
# - Cloudflare Observatory for integrated testingLatency budget template
| Component | Budget | Notes |
|---|---|---|
| DNS | 0ms | Should be cached or use CDN DNS |
| TCP + TLS | 0-20ms | Edge termination, connection reuse |
| Edge processing | 5-10ms | Worker/function execution |
| Cache lookup | 1-5ms | Edge cache hit |
| Origin (on miss) | 50-200ms | Budget this as exception, not norm |
| Content transfer | 10-50ms | Depends on payload size |
| Total (cache hit) | 5-35ms | Target for majority of requests |
| Total (cache miss) | 60-250ms | Acceptable for dynamic content |
Aim for 90%+ cache hit rate to keep the average close to the cache-hit budget.
Frequently Asked Questions
What is edge-computing?
Use this skill when deploying edge functions, writing Cloudflare Workers, configuring CDN cache logic, optimizing latency with edge-side processing, or building serverless-at-the-edge architectures. Triggers on edge functions, CDN rules, Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Lambda@Edge, cache headers, geo-routing, and any task requiring computation close to the user.
How do I install edge-computing?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill edge-computing in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support edge-computing?
edge-computing works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.