---
name: edge-patterns
description: "Edge Computing patterns: Cloudflare Workers (Durable Objects, KV, D1, R2, Wrangler), Vercel Edge Middleware (geo-routing, A/B testing, Edge Config), Deno Deploy, edge runtime constraints (no fs/child_process, bundle limits), streaming responses, edge caching (Cache API, stale-while-revalidate), and testing with Miniflare."
---

# Edge Patterns

Reference for building on edge runtimes — Cloudflare Workers, Vercel Edge, and Deno Deploy.

## When to Activate

- Building Cloudflare Workers or Pages Functions
- Writing Vercel Edge Middleware
- Deploying to Deno Deploy
- Debugging "not supported in edge runtime" errors
- Implementing geo-routing, A/B testing, or auth at the edge
- Setting up edge caching strategies
- Migrating a Node.js API route to an edge-compatible runtime to reduce cold-start latency
- Designing stateful edge logic using Durable Objects or Deno KV
- Choosing between Cloudflare, Vercel Edge, and Deno Deploy for a new globally distributed service

---

## Edge Runtime Constraints

Edge runtimes execute in V8 isolates — not Node.js. Key differences:

| Feature | Node.js | Edge Runtime |
|---------|---------|--------------|
| File system | `fs` module | ❌ Not available |
| Child processes | `child_process` | ❌ Not available |
| TCP sockets | `net`, `dgram` | ❌ Not available |
| Native modules | `node-gyp` bindings | ❌ Not available |
| npm packages | All | Only Web-API compatible |
| Bundle size | Unlimited | Cloudflare: 1MB / Vercel: 4MB |
| CPU time | Unlimited | Cloudflare: 50ms / Vercel: 25s wall |
| Cold starts | Seconds | Milliseconds (isolate reuse) |

**Available Web Standard APIs:**
- `fetch`, `Request`, `Response`, `Headers`
- `URL`, `URLSearchParams`
- `crypto.subtle` (Web Crypto API)
- `ReadableStream`, `WritableStream`, `TransformStream`
- `TextEncoder`, `TextDecoder`
- `setTimeout`, `setInterval` (limited in Workers)
- `Blob`, `FormData`

---

## Cloudflare Workers

### Worker Anatomy

```typescript
// src/index.ts
export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    // env — bound resources (KV, D1, R2, secrets)
    // ctx — lifecycle methods (waitUntil, passThroughOnException)

    const url = new URL(request.url);

    if (url.pathname === '/api/health') {
      return Response.json({ status: 'ok' });
    }

    return new Response('Not Found', { status: 404 });
  },
};

// Type bindings (generated by wrangler)
interface Env {
  MY_KV: KVNamespace;
  MY_DB: D1Database;
  MY_BUCKET: R2Bucket;
  MY_SECRET: string;          // from wrangler.toml [vars] or Secrets
}
```

### wrangler.toml

```toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
preview_id = "def456"

[[d1_databases]]
binding = "MY_DB"
database_name = "my-db"
database_id = "uuid-here"

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"

[vars]
ENVIRONMENT = "production"
```

### KV (Key-Value Store)

```typescript
// Write
await env.MY_KV.put('user:123', JSON.stringify(user), {
  expirationTtl: 3600,  // seconds
});

// Read
const raw = await env.MY_KV.get('user:123');
const user = raw ? JSON.parse(raw) : null;

// Read as JSON (shorthand)
const user = await env.MY_KV.get<User>('user:123', 'json');

// Delete
await env.MY_KV.delete('user:123');

// List keys
const { keys, list_complete } = await env.MY_KV.list({ prefix: 'user:' });
```

### D1 (SQLite at Edge)

```typescript
// Query
const { results } = await env.MY_DB.prepare(
  'SELECT * FROM users WHERE id = ?'
).bind(userId).all<User>();

// Insert
const { success } = await env.MY_DB.prepare(
  'INSERT INTO users (name, email) VALUES (?, ?)'
).bind(name, email).run();

// Batch (transactional)
const [create, notify] = await env.MY_DB.batch([
  env.MY_DB.prepare('INSERT INTO users (name) VALUES (?)').bind(name),
  env.MY_DB.prepare('INSERT INTO events (type, user) VALUES (?, ?)').bind('created', name),
]);
```

### R2 (Object Storage)

```typescript
// Upload
await env.MY_BUCKET.put(key, request.body, {
  httpMetadata: { contentType: 'image/png' },
  customMetadata: { uploadedBy: userId },
});

// Download
const object = await env.MY_BUCKET.get(key);
if (!object) return new Response('Not Found', { status: 404 });

return new Response(object.body, {
  headers: {
    'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
    'ETag': object.etag,
  },
});
```

### Durable Objects (Stateful Edge)

```typescript
// Durable Object class — co-located storage + compute
export class SessionRoom {
  private state: DurableObjectState;
  private sessions: Map<string, WebSocket> = new Map();

  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
    // Hibernatable WebSocket API — don't hold connections during idle
    this.state.acceptWebSocket(/* ... */);
  }

  async fetch(request: Request): Promise<Response> {
    const { pathname } = new URL(request.url);

    if (pathname === '/websocket') {
      const pair = new WebSocketPair();
      this.state.acceptWebSocket(pair[1]);
      return new Response(null, { status: 101, webSocket: pair[0] });
    }

    // Durable storage — persists across invocations
    const count = (await this.state.storage.get<number>('count') ?? 0) + 1;
    await this.state.storage.put('count', count);
    return Response.json({ count });
  }

  webSocketMessage(ws: WebSocket, message: string) {
    // Broadcast to all connected clients
    for (const session of this.state.getWebSockets()) {
      session.send(message);
    }
  }
}

// wrangler.toml
// [[durable_objects.bindings]]
// name = "SESSION_ROOM"
// class_name = "SessionRoom"
```

### waitUntil — Background Tasks

```typescript
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const response = await handleRequest(request, env);

    // Run after response is sent — doesn't block the response
    ctx.waitUntil(logAnalytics(request, response, env));
    ctx.waitUntil(updateCache(request, env));

    return response;
  },
};
```

### Wrangler Dev Workflow

```bash
# Local dev (runs in Workerd, same runtime as production)
wrangler dev

# Deploy to production
wrangler deploy

# Deploy to staging
wrangler deploy --env staging

# Tail production logs
wrangler tail

# D1 migrations
wrangler d1 migrations create my-db add-users-table
wrangler d1 migrations apply my-db

# KV operations
wrangler kv key put --binding=MY_KV "key" "value"
wrangler kv key get --binding=MY_KV "key"
```

---

## Vercel Edge Middleware

```typescript
// middleware.ts — runs before every matched request
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname, searchParams } = request.nextUrl;

  // Geo-based routing
  const country = request.geo?.country ?? 'US';
  if (pathname === '/pricing' && country === 'DE') {
    return NextResponse.rewrite(new URL('/pricing/eu', request.url));
  }

  // A/B testing
  const bucket = request.cookies.get('ab-bucket')?.value
    ?? (Math.random() < 0.5 ? 'a' : 'b');

  const response = NextResponse.next();
  response.cookies.set('ab-bucket', bucket, { maxAge: 86400 });

  if (bucket === 'b') {
    response.headers.set('x-experiment', 'variant-b');
  }

  return response;
}

// Only run middleware on these paths
export const config = {
  matcher: [
    '/pricing/:path*',
    '/checkout/:path*',
    '/((?!api|_next|static|favicon).*)',
  ],
};
```

### Edge Config (Feature Flags at Edge)

```typescript
import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  const maintenanceMode = await get<boolean>('maintenance_mode');

  if (maintenanceMode) {
    return NextResponse.rewrite(new URL('/maintenance', request.url));
  }

  return NextResponse.next();
}
```

```bash
# Update without deployment
vercel env pull .env.local
vercel edge-config update '{"maintenance_mode": true}'
```

---

## Deno Deploy

```typescript
// main.ts — Deno-native edge function
Deno.serve(async (req) => {
  const url = new URL(req.url);

  if (url.pathname === '/api/hello') {
    return Response.json({ message: 'Hello from Deno Deploy!' });
  }

  return new Response('Not Found', { status: 404 });
});
```

### Deno KV

```typescript
const kv = await Deno.openKv();

// Set
await kv.set(['users', userId], { name, email }, { expireIn: 3600_000 });

// Get
const entry = await kv.get<User>(['users', userId]);
const user = entry.value;

// Atomic operation
const result = await kv.atomic()
  .check({ key: ['counter'], versionstamp: null })
  .set(['counter'], 0)
  .commit();
```

### Deploy via GitHub Actions

```yaml
name: Deploy to Deno Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/deployctl@v1
        with:
          project: my-deno-project
          entrypoint: main.ts
```

---

For streaming responses (TransformStream, Server-Sent Events), edge caching (Cache API, stale-while-revalidate), Miniflare testing, and common mistakes, see skill `edge-patterns-advanced`.

## Reference

- `serverless-patterns` — Cold starts, Step Functions, Lambda Powertools, idempotency
- `deployment-patterns` — Docker, CI/CD, rollback strategies
- `wasm-patterns` — Run Rust WASM in Cloudflare Workers
