This commit is contained in:
admin
2026-01-30 03:04:10 +00:00
parent bcc4d242c4
commit 2a3dedde11
1218 changed files with 214731 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"source": "github.com/cloudflare/skills/tree/main/skills/cloudflare",
"type": "github-subdir",
"installed_at": "2026-01-30T02:29:56.55431773Z",
"repo_url": "https://github.com/cloudflare/skills.git",
"subdir": "skills/cloudflare",
"version": "75a603b"
}

201
cloudflare/SKILL.md Normal file
View File

@@ -0,0 +1,201 @@
---
name: cloudflare
description: Comprehensive Cloudflare platform skill covering Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking (Tunnel, Spectrum), security (WAF, DDoS), and infrastructure-as-code (Terraform, Pulumi). Use for any Cloudflare development task.
references:
- workers
- pages
- d1
- durable-objects
- workers-ai
---
# Cloudflare Platform Skill
Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references.
## Quick Decision Trees
### "I need to run code"
```
Need to run code?
├─ Serverless functions at the edge → workers/
├─ Full-stack web app with Git deploys → pages/
├─ Stateful coordination/real-time → durable-objects/
├─ Long-running multi-step jobs → workflows/
├─ Run containers → containers/
├─ Multi-tenant (customers deploy code) → workers-for-platforms/
├─ Scheduled tasks (cron) → cron-triggers/
├─ Lightweight edge logic (modify HTTP) → snippets/
├─ Process Worker execution events (logs/observability) → tail-workers/
└─ Optimize latency to backend infrastructure → smart-placement/
```
### "I need to store data"
```
Need storage?
├─ Key-value (config, sessions, cache) → kv/
├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL)
├─ Object/file storage (S3-compatible) → r2/
├─ Message queue (async processing) → queues/
├─ Vector embeddings (AI/semantic search) → vectorize/
├─ Strongly-consistent per-entity state → durable-objects/ (DO storage)
├─ Secrets management → secrets-store/
├─ Streaming ETL to R2 → pipelines/
└─ Persistent cache (long-term retention) → cache-reserve/
```
### "I need AI/ML"
```
Need AI?
├─ Run inference (LLMs, embeddings, images) → workers-ai/
├─ Vector database for RAG/search → vectorize/
├─ Build stateful AI agents → agents-sdk/
├─ Gateway for any AI provider (caching, routing) → ai-gateway/
└─ AI-powered search widget → ai-search/
```
### "I need networking/connectivity"
```
Need networking?
├─ Expose local service to internet → tunnel/
├─ TCP/UDP proxy (non-HTTP) → spectrum/
├─ WebRTC TURN server → turn/
├─ Private network connectivity → network-interconnect/
├─ Optimize routing → argo-smart-routing/
├─ Optimize latency to backend (not user) → smart-placement/
└─ Real-time video/audio → realtimekit/ or realtime-sfu/
```
### "I need security"
```
Need security?
├─ Web Application Firewall → waf/
├─ DDoS protection → ddos/
├─ Bot detection/management → bot-management/
├─ API protection → api-shield/
├─ CAPTCHA alternative → turnstile/
└─ Credential leak detection → waf/ (managed ruleset)
```
### "I need media/content"
```
Need media?
├─ Image optimization/transformation → images/
├─ Video streaming/encoding → stream/
├─ Browser automation/screenshots → browser-rendering/
└─ Third-party script management → zaraz/
```
### "I need infrastructure-as-code"
```
Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API)
```
## Product Index
### Compute & Runtime
| Product | Reference |
|---------|-----------|
| Workers | `references/workers/` |
| Pages | `references/pages/` |
| Pages Functions | `references/pages-functions/` |
| Durable Objects | `references/durable-objects/` |
| Workflows | `references/workflows/` |
| Containers | `references/containers/` |
| Workers for Platforms | `references/workers-for-platforms/` |
| Cron Triggers | `references/cron-triggers/` |
| Tail Workers | `references/tail-workers/` |
| Snippets | `references/snippets/` |
| Smart Placement | `references/smart-placement/` |
### Storage & Data
| Product | Reference |
|---------|-----------|
| KV | `references/kv/` |
| D1 | `references/d1/` |
| R2 | `references/r2/` |
| Queues | `references/queues/` |
| Hyperdrive | `references/hyperdrive/` |
| DO Storage | `references/do-storage/` |
| Secrets Store | `references/secrets-store/` |
| Pipelines | `references/pipelines/` |
| R2 Data Catalog | `references/r2-data-catalog/` |
| R2 SQL | `references/r2-sql/` |
### AI & Machine Learning
| Product | Reference |
|---------|-----------|
| Workers AI | `references/workers-ai/` |
| Vectorize | `references/vectorize/` |
| Agents SDK | `references/agents-sdk/` |
| AI Gateway | `references/ai-gateway/` |
| AI Search | `references/ai-search/` |
### Networking & Connectivity
| Product | Reference |
|---------|-----------|
| Tunnel | `references/tunnel/` |
| Spectrum | `references/spectrum/` |
| TURN | `references/turn/` |
| Network Interconnect | `references/network-interconnect/` |
| Argo Smart Routing | `references/argo-smart-routing/` |
| Workers VPC | `references/workers-vpc/` |
### Security
| Product | Reference |
|---------|-----------|
| WAF | `references/waf/` |
| DDoS Protection | `references/ddos/` |
| Bot Management | `references/bot-management/` |
| API Shield | `references/api-shield/` |
| Turnstile | `references/turnstile/` |
### Media & Content
| Product | Reference |
|---------|-----------|
| Images | `references/images/` |
| Stream | `references/stream/` |
| Browser Rendering | `references/browser-rendering/` |
| Zaraz | `references/zaraz/` |
### Real-Time Communication
| Product | Reference |
|---------|-----------|
| RealtimeKit | `references/realtimekit/` |
| Realtime SFU | `references/realtime-sfu/` |
### Developer Tools
| Product | Reference |
|---------|-----------|
| Wrangler | `references/wrangler/` |
| Miniflare | `references/miniflare/` |
| C3 | `references/c3/` |
| Observability | `references/observability/` |
| Analytics Engine | `references/analytics-engine/` |
| Web Analytics | `references/web-analytics/` |
| Sandbox | `references/sandbox/` |
| Workerd | `references/workerd/` |
| Workers Playground | `references/workers-playground/` |
### Infrastructure as Code
| Product | Reference |
|---------|-----------|
| Pulumi | `references/pulumi/` |
| Terraform | `references/terraform/` |
| API | `references/api/` |
### Other Services
| Product | Reference |
|---------|-----------|
| Email Routing | `references/email-routing/` |
| Email Workers | `references/email-workers/` |
| Static Assets | `references/static-assets/` |
| Bindings | `references/bindings/` |
| Cache Reserve | `references/cache-reserve/` |

View File

@@ -0,0 +1,89 @@
# Cloudflare Agents SDK
Cloudflare Agents SDK enables building AI-powered agents on Durable Objects with state, WebSockets, SQL, scheduling, and AI integration.
## Core Value
Build stateful, globally distributed AI agents with persistent memory, real-time connections, scheduled tasks, and async workflows.
## When to Use
- Persistent state + memory required
- Real-time WebSocket connections
- Long-running workflows (minutes/hours)
- Chat interfaces with AI models
- Scheduled/recurring tasks with state
- DB queries with agent state
## What Type of Agent?
| Use Case | Class | Key Features |
|----------|-------|--------------|
| AI chat interface | `AIChatAgent` | Auto-streaming, tools, message history, resumable |
| MCP tool provider | `Agent` + MCP | Expose tools to AI systems |
| Custom logic/routing | `Agent` | Full control, WebSockets, email, SQL |
| Real-time collaboration | `Agent` | WebSocket state, broadcasts |
| Email processing | `Agent` | `onEmail()` handler |
## Quick Start
**AI Chat Agent:**
```typescript
import { AIChatAgent } from "agents";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages,
onFinish,
});
}
}
```
**Base Agent:**
```typescript
import { Agent } from "agents";
export class MyAgent extends Agent<Env> {
onStart() {
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
}
async onRequest(request: Request) {
return Response.json({ state: this.state });
}
}
```
## Reading Order
| Task | Files to Read |
|------|---------------|
| Quick start | README only |
| Build chat agent | README → api.md (AIChatAgent) → patterns.md |
| Setup project | README → configuration.md |
| Add React frontend | README → api.md (Client Hooks) → patterns.md |
| Build MCP server | api.md (MCP) → patterns.md |
| Background tasks | api.md (Scheduling, Task Queue) → patterns.md |
| Debug issues | gotchas.md |
## Package Entry Points
| Import | Purpose |
|--------|---------|
| `agents` | Server-side Agent classes, lifecycle |
| `agents/react` | `useAgent()` hook for WebSocket connections |
| `agents/ai-react` | `useAgentChat()` hook for AI chat UIs |
## In This Reference
- [configuration.md](./configuration.md) - SDK setup, wrangler config, routing
- [api.md](./api.md) - Agent classes, lifecycle, client hooks
- [patterns.md](./patterns.md) - Common workflows, best practices
- [gotchas.md](./gotchas.md) - Common issues, limits
## See Also
- durable-objects - Agent infrastructure
- d1 - External database integration
- workers-ai - AI model integration
- vectorize - Vector search for RAG patterns

View File

@@ -0,0 +1,190 @@
# API Reference
## Agent Classes
### AIChatAgent
For AI chat with auto-streaming, message history, tools, resumable streaming.
```ts
import { AIChatAgent } from "agents";
import { openai } from "@ai-sdk/openai";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages, // Auto-managed message history
tools: {
getWeather: {
description: "Get weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `Sunny, 72°F in ${city}`
}
},
onFinish, // Persist response to this.messages
});
}
}
```
### Agent (Base Class)
Full control for custom logic, WebSockets, email, and SQL.
```ts
import { Agent } from "agents";
export class MyAgent extends Agent<Env, State> {
// Lifecycle methods below
}
```
**Type params:** `Agent<Env, State, ConnState>` - Env bindings, agent state, connection state
## Lifecycle Hooks
```ts
onStart() { // Init/restart
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT, name TEXT)`;
}
async onRequest(req: Request) { // HTTP
const {pathname} = new URL(req.url);
if (pathname === "/users") return Response.json(this.sql<{id,name}>`SELECT * FROM users`);
return new Response("Not found", {status: 404});
}
async onConnect(conn: Connection<ConnState>, ctx: ConnectionContext) { // WebSocket
conn.accept();
conn.setState({userId: ctx.request.headers.get("X-User-ID")});
conn.send(JSON.stringify({type: "connected", state: this.state}));
}
async onMessage(conn: Connection<ConnState>, msg: WSMessage) { // WS messages
const m = JSON.parse(msg as string);
this.setState({messages: [...this.state.messages, m]});
this.connections.forEach(c => c.send(JSON.stringify(m)));
}
async onEmail(email: AgentEmail) { // Email routing
this.sql`INSERT INTO emails (from_addr,subject,body) VALUES (${email.from},${email.headers.get("subject")},${await email.text()})`;
}
```
## State, SQL, Scheduling
```ts
// State
this.setState({count: 42}); // Auto-syncs
this.setState({...this.state, count: this.state.count + 1});
// SQL (parameterized queries prevent injection)
this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;
this.sql`INSERT INTO users (id,name) VALUES (${userId},${name})`;
const users = this.sql<{id,name}>`SELECT * FROM users WHERE id = ${userId}`;
// Scheduling
await this.schedule(new Date("2026-12-25"), "sendGreeting", {msg:"Hi"}); // Date
await this.schedule(60, "checkStatus", {}); // Delay (sec)
await this.schedule("0 0 * * *", "dailyCleanup", {}); // Cron
await this.cancelSchedule(scheduleId);
```
## RPC Methods (@callable)
```ts
import { Agent, callable } from "agents";
export class MyAgent extends Agent<Env> {
@callable()
async processTask(input: {text: string}): Promise<{result: string}> {
return { result: await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt: input.text}) };
}
}
// Client: const result = await agent.processTask({ text: "Hello" });
// Must return JSON-serializable values
```
## Connections & AI
```ts
// Connections (type: Agent<Env, State, ConnState>)
this.connections.forEach(c => c.send(JSON.stringify(msg))); // Broadcast
conn.setState({userId:"123"}); conn.close(1000, "Goodbye");
// Workers AI
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
// Manual streaming (prefer AIChatAgent)
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
for await (const chunk of stream) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
```
**Type-safe state:** `Agent<Env, State, ConnState>` - third param types `conn.state`
## MCP Integration
Model Context Protocol for exposing tools:
```ts
// Register & use MCP server
await this.mcp.registerServer("github", {
url: env.MCP_SERVER_URL,
auth: { type: "oauth", clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }
});
const tools = await this.mcp.getAITools(["github"]);
return this.streamText({ model: openai("gpt-4"), messages: this.messages, tools, onFinish });
```
## Task Queue
```ts
await this.queue("processVideo", { videoId: "abc123" }); // Add task
const tasks = await this.dequeue(10); // Process up to 10
```
## Context & Cleanup
```ts
const agent = getCurrentAgent<MyAgent>(); // Get current instance
async destroy() { /* cleanup before agent destroyed */ }
```
## AI Integration
```ts
// Workers AI
const r = await this.env.AI.run("@cf/meta/llama-3.1-8b-instruct", {prompt});
// Manual streaming (prefer AIChatAgent for auto-streaming)
const stream = await client.chat.completions.create({model: "gpt-4", messages, stream: true});
for await (const chunk of stream) {
if (chunk.choices[0]?.delta?.content) conn.send(JSON.stringify({chunk: chunk.choices[0].delta.content}));
}
```
## Client Hooks (React)
```ts
// useAgent() - WebSocket connection + RPC
import { useAgent } from "agents/react";
const agent = useAgent({ agent: "MyAgent", name: "user-123" }); // name for idFromName
const result = await agent.processTask({ text: "Hello" }); // Call @callable methods
// agent.readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
// useAgentChat() - AI chat UI
import { useAgentChat } from "agents/ai-react";
const agent = useAgent({ agent: "ChatAgent" });
const { messages, input, handleInputChange, handleSubmit, isLoading, stop, clearHistory } =
useAgentChat({
agent,
maxSteps: 5, // Max tool iterations
resume: true, // Auto-resume on disconnect
onToolCall: async (toolCall) => {
// Client tools (human-in-the-loop)
if (toolCall.toolName === "confirm") return { ok: window.confirm("Proceed?") };
}
});
// status: "ready" | "submitted" | "streaming" | "error"
```

View File

@@ -0,0 +1,182 @@
# Configuration
## Wrangler Setup
```jsonc
{
"name": "my-agents-app",
"durable_objects": {
"bindings": [
{"name": "MyAgent", "class_name": "MyAgent"}
]
},
"migrations": [
{"tag": "v1", "new_sqlite_classes": ["MyAgent"]}
],
"ai": {
"binding": "AI"
}
}
```
## Environment Bindings
**Type-safe pattern:**
```typescript
interface Env {
AI?: Ai; // Workers AI
MyAgent?: DurableObjectNamespace<MyAgent>;
ChatAgent?: DurableObjectNamespace<ChatAgent>;
DB?: D1Database; // D1 database
KV?: KVNamespace; // KV storage
R2?: R2Bucket; // R2 bucket
OPENAI_API_KEY?: string; // Secrets
GITHUB_CLIENT_ID?: string; // MCP OAuth credentials
GITHUB_CLIENT_SECRET?: string;
QUEUE?: Queue; // Queues
}
```
**Best practice:** Define all DO bindings in Env interface for type safety.
## Deployment
```bash
# Local dev
npx wrangler dev
# Deploy production
npx wrangler deploy
# Set secrets
npx wrangler secret put OPENAI_API_KEY
```
## Agent Routing
**Recommended: Use route helpers**
```typescript
import { routeAgent } from "agents";
export default {
fetch(request: Request, env: Env) {
return routeAgent(request, env);
}
}
```
Helper routes requests to agents automatically based on URL patterns.
**Manual routing (advanced):**
```typescript
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Named ID (deterministic)
const id = env.MyAgent.idFromName("user-123");
// Random ID (from URL param)
// const id = env.MyAgent.idFromString(url.searchParams.get("id"));
const stub = env.MyAgent.get(id);
return stub.fetch(request);
}
}
```
**Multi-agent setup:**
```typescript
import { routeAgent } from "agents";
export default {
fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Route by path
if (url.pathname.startsWith("/chat")) {
return routeAgent(request, env, "ChatAgent");
}
if (url.pathname.startsWith("/task")) {
return routeAgent(request, env, "TaskAgent");
}
return new Response("Not found", { status: 404 });
}
}
```
## Email Routing
**Code setup:**
```typescript
import { routeAgentEmail } from "agents";
export default {
fetch: (req: Request, env: Env) => routeAgent(req, env),
email: (message: ForwardableEmailMessage, env: Env) => {
return routeAgentEmail(message, env);
}
}
```
**Dashboard setup:**
Configure email routing in Cloudflare dashboard:
```
Destination: Workers with Durable Objects
Worker: my-agents-app
```
Then handle in agent:
```typescript
export class EmailAgent extends Agent<Env> {
async onEmail(email: AgentEmail) {
const text = await email.text();
// Process email
}
}
```
## AI Gateway (Optional)
```typescript
// Enable caching/routing through AI Gateway
const response = await this.env.AI.run(
"@cf/meta/llama-3.1-8b-instruct",
{ prompt },
{
gateway: {
id: "my-gateway-id",
skipCache: false,
cacheTtl: 3600
}
}
);
```
## MCP Configuration (Optional)
For exposing tools via Model Context Protocol:
```typescript
// wrangler.jsonc - Add MCP OAuth secrets
{
"vars": {
"MCP_SERVER_URL": "https://mcp.example.com"
}
}
// Set secrets via CLI
// npx wrangler secret put GITHUB_CLIENT_ID
// npx wrangler secret put GITHUB_CLIENT_SECRET
```
Then register in agent code (see api.md MCP section).

View File

@@ -0,0 +1,158 @@
# Gotchas & Best Practices
## Common Errors
### "setState() not syncing"
**Cause:** Mutating state directly or not calling `setState()` after modifications
**Solution:** Always use `setState()` with immutable updates:
```ts
// ❌ this.state.count++
// ✅ this.setState({...this.state, count: this.state.count + 1})
```
### "Message history grows unbounded (AIChatAgent)"
**Cause:** `this.messages` in `AIChatAgent` accumulates all messages indefinitely
**Solution:** Manually trim old messages periodically:
```ts
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
// Keep only last 50 messages
if (this.messages.length > 50) {
this.messages = this.messages.slice(-50);
}
return this.streamText({ model: openai("gpt-4"), messages: this.messages, onFinish });
}
}
```
### "SQL injection vulnerability"
**Cause:** Direct string interpolation in SQL queries
**Solution:** Use parameterized queries:
```ts
// ❌ this.sql`...WHERE id = '${userId}'`
// ✅ this.sql`...WHERE id = ${userId}`
```
### "WebSocket connection timeout"
**Cause:** Not calling `conn.accept()` in `onConnect`
**Solution:** Always accept connections:
```ts
async onConnect(conn: Connection, ctx: ConnectionContext) { conn.accept(); conn.setState({userId: "123"}); }
```
### "Schedule limit exceeded"
**Cause:** More than 1000 scheduled tasks per agent
**Solution:** Clean up old schedules and limit creation rate:
```ts
async checkSchedules() { if ((await this.getSchedules()).length > 800) console.warn("Near limit!"); }
```
### "AI Gateway unavailable"
**Cause:** AI service timeout or quota exceeded
**Solution:** Add error handling and fallbacks:
```ts
try {
return await this.env.AI.run(model, {prompt});
} catch (e) {
console.error("AI error:", e);
return {error: "Unavailable"};
}
```
### "@callable method returns undefined"
**Cause:** Method doesn't return JSON-serializable value, or has non-serializable types
**Solution:** Ensure return values are plain objects/arrays/primitives:
```ts
// ❌ Returns class instance
@callable()
async getData() { return new Date(); }
// ✅ Returns serializable object
@callable()
async getData() { return { timestamp: Date.now() }; }
```
### "Resumable stream not resuming"
**Cause:** Stream ID must be deterministic for resumption to work
**Solution:** Use AIChatAgent (automatic) or ensure consistent stream IDs:
```ts
// AIChatAgent handles this automatically
export class ChatAgent extends AIChatAgent<Env> {
// Resumption works out of the box
}
```
### "MCP connection loss on hibernation"
**Cause:** MCP server connections don't survive hibernation
**Solution:** Re-register servers in `onStart()` or check connection status:
```ts
onStart() {
// Re-register MCP servers after hibernation
await this.mcp.registerServer("github", { url: env.MCP_URL, auth: {...} });
}
```
### "Agent not found"
**Cause:** Durable Object binding missing or incorrect class name
**Solution:** Verify DO binding in wrangler.jsonc and class name matches
## Rate Limits & Quotas
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| CPU per request | 30s (std), 300s (max) | Set in wrangler.jsonc |
| Memory per instance | 128MB | Shared with WebSockets |
| Storage per agent | 10GB | SQLite storage |
| Scheduled tasks | 1000 per agent | Monitor with `getSchedules()` |
| WebSocket connections | Unlimited | Within memory limits |
| SQL columns | 100 | Per table |
| SQL row size | 2MB | Key + value |
| WebSocket message | 32MiB | Max size |
| DO requests/sec | ~1000 | Per unique DO instance; rate limit if needed |
| AI Gateway (Workers AI) | Model-specific | Check dashboard for limits |
| MCP requests | Depends on server | Implement retry/backoff |
## Best Practices
### State Management
- Use immutable updates: `setState({...this.state, key: newValue})`
- Trim unbounded arrays (messages, logs) periodically
- Store large data in SQL, not state
### SQL Usage
- Create tables in `onStart()`, not `onRequest()`
- Use parameterized queries: `` sql`WHERE id = ${id}` `` (NOT `` sql`WHERE id = '${id}'` ``)
- Index frequently queried columns
### Scheduling
- Monitor schedule count: `await this.getSchedules()`
- Cancel completed tasks to stay under 1000 limit
- Use cron strings for recurring tasks
### WebSockets
- Always call `conn.accept()` in `onConnect()`
- Handle client disconnects gracefully
- Broadcast to `this.connections` efficiently
### AI Integration
- Use `AIChatAgent` for chat interfaces (auto-streaming, resumption)
- Trim message history to avoid token limits
- Handle AI errors with try/catch and fallbacks
### Production Deployment
- **Rate limiting:** Implement request throttling for high-traffic agents (>1000 req/s)
- **Monitoring:** Log critical errors, track schedule count, monitor storage usage
- **Graceful degradation:** Handle AI service outages with fallbacks
- **Message trimming:** Enforce max history length (e.g., 100 messages) in AIChatAgent
- **MCP reliability:** Re-register servers on hibernation, implement retry logic

View File

@@ -0,0 +1,192 @@
# Patterns & Use Cases
## AI Chat w/Tools
**Server (AIChatAgent):**
```ts
import { AIChatAgent } from "agents";
import { openai } from "@ai-sdk/openai";
import { tool } from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages, // Auto-managed
tools: {
getWeather: tool({
description: "Get current weather",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `Weather in ${city}: Sunny, 72°F`
}),
searchDocs: tool({
description: "Search documentation",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => JSON.stringify(
this.sql<{title, content}>`SELECT title, content FROM docs WHERE content LIKE ${'%' + query + '%'}`
)
})
},
onFinish,
});
}
}
```
**Client (React):**
```tsx
import { useAgent } from "agents/react";
import { useAgentChat } from "agents/ai-react";
function ChatUI() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, input, handleInputChange, handleSubmit, isLoading } = useAgentChat({ agent });
return (
<div>
{messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} disabled={isLoading} />
<button disabled={isLoading}>Send</button>
</form>
</div>
);
}
```
## Human-in-the-Loop (Client Tools)
Server defines tool, client executes:
```ts
// Server
export class ChatAgent extends AIChatAgent<Env> {
async onChatMessage(onFinish) {
return this.streamText({
model: openai("gpt-4"),
messages: this.messages,
tools: {
confirmAction: tool({
description: "Ask user to confirm",
parameters: z.object({ action: z.string() }),
execute: "client", // Client-side execution
})
},
onFinish,
});
}
}
// Client
const { messages } = useAgentChat({
agent,
onToolCall: async (toolCall) => {
if (toolCall.toolName === "confirmAction") {
return { confirmed: window.confirm(`Confirm: ${toolCall.args.action}?`) };
}
}
});
```
## Task Queue & Scheduled Processing
```ts
export class TaskAgent extends Agent<Env> {
onStart() {
this.schedule("*/5 * * * *", "processQueue", {}); // Every 5 min
this.schedule("0 0 * * *", "dailyCleanup", {}); // Daily
}
async onRequest(req: Request) {
await this.queue("processVideo", { videoId: (await req.json()).videoId });
return Response.json({ queued: true });
}
async processQueue() {
const tasks = await this.dequeue(10);
for (const task of tasks) {
if (task.name === "processVideo") await this.processVideo(task.data.videoId);
}
}
async dailyCleanup() {
this.sql`DELETE FROM logs WHERE created_at < ${Date.now() - 86400000}`;
}
}
```
## Manual WebSocket Chat
Custom protocols (non-AI):
```ts
export class ChatAgent extends Agent<Env> {
async onConnect(conn: Connection, ctx: ConnectionContext) {
conn.accept();
conn.setState({userId: ctx.request.headers.get("X-User-ID") || "anon"});
conn.send(JSON.stringify({type: "history", messages: this.state.messages}));
}
async onMessage(conn: Connection, msg: WSMessage) {
const newMsg = {userId: conn.state.userId, text: JSON.parse(msg as string).text, timestamp: Date.now()};
this.setState({messages: [...this.state.messages, newMsg]});
this.connections.forEach(c => c.send(JSON.stringify(newMsg)));
}
}
```
## Email Processing w/AI
```ts
export class EmailAgent extends Agent<Env> {
async onEmail(email: AgentEmail) {
const [text, from, subject] = [await email.text(), email.from, email.headers.get("subject") || ""];
this.sql`INSERT INTO emails (from_addr, subject, body) VALUES (${from}, ${subject}, ${text})`;
const { text: summary } = await generateText({
model: openai("gpt-4o-mini"), prompt: `Summarize: ${subject}\n\n${text}`
});
this.connections.forEach(c => c.send(JSON.stringify({type: "new_email", from, summary})));
if (summary.includes("urgent")) await this.schedule(0, "sendAutoReply", { to: from });
}
}
```
## Real-time Collaboration
```ts
export class GameAgent extends Agent<Env> {
initialState = { players: [], gameStarted: false };
async onConnect(conn: Connection, ctx: ConnectionContext) {
conn.accept();
const playerId = ctx.request.headers.get("X-Player-ID") || crypto.randomUUID();
conn.setState({ playerId });
const newPlayer = { id: playerId, score: 0 };
this.setState({...this.state, players: [...this.state.players, newPlayer]});
this.connections.forEach(c => c.send(JSON.stringify({type: "player_joined", player: newPlayer})));
}
async onMessage(conn: Connection, msg: WSMessage) {
const m = JSON.parse(msg as string);
if (m.type === "move") {
this.setState({
...this.state,
players: this.state.players.map(p => p.id === conn.state.playerId ? {...p, score: p.score + m.points} : p)
});
this.connections.forEach(c => c.send(JSON.stringify({type: "player_moved", playerId: conn.state.playerId})));
}
if (m.type === "start" && this.state.players.length >= 2) {
this.setState({...this.state, gameStarted: true});
this.connections.forEach(c => c.send(JSON.stringify({type: "game_started"})));
}
}
}
```

View File

@@ -0,0 +1,175 @@
# Cloudflare AI Gateway
Expert guidance for implementing Cloudflare AI Gateway - a universal gateway for AI model providers with analytics, caching, rate limiting, and routing capabilities.
## When to Use This Reference
- Setting up AI Gateway for any AI provider (OpenAI, Anthropic, Workers AI, etc.)
- Implementing caching, rate limiting, or request retry/fallback
- Configuring dynamic routing with A/B testing or model fallbacks
- Managing provider API keys securely with BYOK
- Adding security features (guardrails, DLP)
- Setting up observability with logging and custom metadata
- Debugging AI Gateway requests or optimizing configurations
## Quick Start
**What's your setup?**
- **Using Vercel AI SDK** → Pattern 1 (recommended) - see [sdk-integration.md](./sdk-integration.md)
- **Using OpenAI SDK** → Pattern 2 - see [sdk-integration.md](./sdk-integration.md)
- **Cloudflare Worker + Workers AI** → Pattern 3 - see [sdk-integration.md](./sdk-integration.md)
- **Direct HTTP (any language)** → Pattern 4 - see [configuration.md](./configuration.md)
- **Framework (LangChain, etc.)** → See [sdk-integration.md](./sdk-integration.md)
## Pattern 1: Vercel AI SDK (Recommended)
Most modern pattern using official `ai-gateway-provider` package with automatic fallbacks.
```typescript
import { createAiGateway } from 'ai-gateway-provider';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
const gateway = createAiGateway({
accountId: process.env.CF_ACCOUNT_ID,
gateway: process.env.CF_GATEWAY_ID,
});
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY
});
// Single model
const { text } = await generateText({
model: gateway(openai('gpt-4o')),
prompt: 'Hello'
});
// Automatic fallback array
const { text } = await generateText({
model: gateway([
openai('gpt-4o'), // Try first
anthropic('claude-sonnet-4-5'), // Fallback
]),
prompt: 'Hello'
});
```
**Install:** `npm install ai-gateway-provider ai @ai-sdk/openai @ai-sdk/anthropic`
## Pattern 2: OpenAI SDK
Drop-in replacement for OpenAI API with multi-provider support.
```typescript
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/compat`,
defaultHeaders: {
'cf-aig-authorization': `Bearer ${cfToken}` // For authenticated gateways
}
});
// Switch providers by changing model format: {provider}/{model}
const response = await client.chat.completions.create({
model: 'openai/gpt-4o', // or 'anthropic/claude-sonnet-4-5'
messages: [{ role: 'user', content: 'Hello!' }]
});
```
## Pattern 3: Workers AI Binding
For Cloudflare Workers using Workers AI.
```typescript
export default {
async fetch(request, env, ctx) {
const response = await env.AI.run(
'@cf/meta/llama-3-8b-instruct',
{ messages: [{ role: 'user', content: 'Hello!' }] },
{
gateway: {
id: 'my-gateway',
metadata: { userId: '123', team: 'engineering' }
}
}
);
return Response.json(response);
}
};
```
## Headers Quick Reference
| Header | Purpose | Example | Notes |
|--------|---------|---------|-------|
| `cf-aig-authorization` | Gateway auth | `Bearer {token}` | Required for authenticated gateways |
| `cf-aig-metadata` | Tracking | `{"userId":"x"}` | Max 5 entries, flat structure |
| `cf-aig-cache-ttl` | Cache duration | `3600` | Seconds, min 60, max 2592000 (30 days) |
| `cf-aig-skip-cache` | Bypass cache | `true` | - |
| `cf-aig-cache-key` | Custom cache key | `my-key` | Must be unique per response |
| `cf-aig-collect-log` | Skip logging | `false` | Default: true |
| `cf-aig-cache-status` | Cache hit/miss | Response only | `HIT` or `MISS` |
## In This Reference
| File | Purpose |
|------|---------|
| [sdk-integration.md](./sdk-integration.md) | Vercel AI SDK, OpenAI SDK, Workers binding patterns |
| [configuration.md](./configuration.md) | Dashboard setup, wrangler, API tokens |
| [features.md](./features.md) | Caching, rate limits, guardrails, DLP, BYOK, unified billing |
| [dynamic-routing.md](./dynamic-routing.md) | Fallbacks, A/B testing, conditional routing |
| [troubleshooting.md](./troubleshooting.md) | Debugging, errors, observability, gotchas |
## Reading Order
| Task | Files |
|------|-------|
| First-time setup | README + [configuration.md](./configuration.md) |
| SDK integration | README + [sdk-integration.md](./sdk-integration.md) |
| Enable caching | README + [features.md](./features.md) |
| Setup fallbacks | README + [dynamic-routing.md](./dynamic-routing.md) |
| Debug errors | README + [troubleshooting.md](./troubleshooting.md) |
## Architecture
AI Gateway acts as a proxy between your application and AI providers:
```
Your App → AI Gateway → AI Provider (OpenAI, Anthropic, etc.)
Analytics, Caching, Rate Limiting, Logging
```
**Key URL patterns:**
- Unified API (OpenAI-compatible): `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/compat/chat/completions`
- Provider-specific: `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/{provider}/{endpoint}`
- Dynamic routes: Use route name instead of model: `dynamic/{route-name}`
## Gateway Types
1. **Unauthenticated Gateway**: Open access (not recommended for production)
2. **Authenticated Gateway**: Requires `cf-aig-authorization` header with Cloudflare API token (recommended)
## Provider Authentication Options
1. **Unified Billing**: Use AI Gateway billing to pay for inference (keyless mode - no provider API key needed)
2. **BYOK (Store Keys)**: Store provider API keys in Cloudflare dashboard
3. **Request Headers**: Include provider API key in each request
## Related Skills
- [Workers AI](../workers-ai/README.md) - For `env.AI.run()` details
- [Agents SDK](../agents-sdk/README.md) - For stateful AI patterns
- [Vectorize](../vectorize/README.md) - For RAG patterns with embeddings
## Resources
- [Official Docs](https://developers.cloudflare.com/ai-gateway/)
- [API Reference](https://developers.cloudflare.com/api/resources/ai_gateway/)
- [Provider Guides](https://developers.cloudflare.com/ai-gateway/usage/providers/)
- [Discord Community](https://discord.cloudflare.com)

View File

@@ -0,0 +1,111 @@
# Configuration & Setup
## Creating a Gateway
### Dashboard
AI > AI Gateway > Create Gateway > Configure (auth, caching, rate limiting, logging)
### API
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
-H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
-d '{"id":"my-gateway","cache_ttl":3600,"rate_limiting_interval":60,"rate_limiting_limit":100,"collect_logs":true}'
```
**Naming:** lowercase alphanumeric + hyphens (e.g., `prod-api`, `dev-chat`)
## Wrangler Integration
```toml
[ai]
binding = "AI"
[[ai.gateway]]
id = "my-gateway"
```
```bash
wrangler secret put CF_API_TOKEN
wrangler secret put OPENAI_API_KEY # If not using BYOK
```
## Authentication
### Gateway Auth (protects gateway access)
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
### Provider Auth Options
**1. Unified Billing (keyless)** - pay through Cloudflare, no provider key:
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
Supports: OpenAI, Anthropic, Google AI Studio
**2. BYOK** - store keys in dashboard (Provider Keys > Add), no key in code
**3. Request Headers** - pass provider key per request:
```typescript
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
## API Token Permissions
- **Gateway management:** AI Gateway - Read + Edit
- **Gateway access:** AI Gateway - Read (minimum)
## Gateway Management API
```bash
# List
curl https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways \
-H "Authorization: Bearer $CF_API_TOKEN"
# Get
curl .../gateways/{gateway_id}
# Update
curl -X PUT .../gateways/{gateway_id} \
-d '{"cache_ttl":7200,"rate_limiting_limit":200}'
# Delete
curl -X DELETE .../gateways/{gateway_id}
```
## Getting IDs
- **Account ID:** Dashboard > Overview > Copy
- **Gateway ID:** AI Gateway > Gateway name column
## Python Example
```python
from openai import OpenAI
import os
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
base_url=f"https://gateway.ai.cloudflare.com/v1/{os.environ['CF_ACCOUNT_ID']}/{os.environ['GATEWAY_ID']}/openai",
default_headers={"cf-aig-authorization": f"Bearer {os.environ['CF_API_TOKEN']}"}
)
```
## Best Practices
1. **Always authenticate gateways in production**
2. **Use BYOK or unified billing** - secrets out of code
3. **Environment-specific gateways** - separate dev/staging/prod
4. **Set rate limits** - prevent runaway costs
5. **Enable logging** - track usage, debug issues

View File

@@ -0,0 +1,82 @@
# Dynamic Routing
Configure complex routing in dashboard without code changes. Use route names instead of model names.
## Usage
```typescript
const response = await client.chat.completions.create({
model: 'dynamic/smart-chat', // Route name from dashboard
messages: [{ role: 'user', content: 'Hello!' }]
});
```
## Node Types
| Node | Purpose | Use Case |
|------|---------|----------|
| **Conditional** | Branch on metadata | Paid vs free users, geo routing |
| **Percentage** | A/B split traffic | Model testing, gradual rollouts |
| **Rate Limit** | Enforce quotas | Per-user/team limits |
| **Budget Limit** | Cost quotas | Per-user spending caps |
| **Model** | Call provider | Final destination |
## Metadata
Pass via header (max 5 entries, flat only):
```typescript
headers: {
'cf-aig-metadata': JSON.stringify({
userId: 'user-123',
tier: 'pro',
region: 'us-east'
})
}
```
## Common Patterns
**Multi-model fallback:**
```
Start → GPT-4 → On error: Claude → On error: Llama
```
**Tiered access:**
```
Conditional: tier == 'enterprise' → GPT-4 (no limit)
Conditional: tier == 'pro' → Rate Limit 1000/hr → GPT-4o
Conditional: tier == 'free' → Rate Limit 10/hr → GPT-4o-mini
```
**Gradual rollout:**
```
Percentage: 10% → New model, 90% → Old model
```
**Cost-based fallback:**
```
Budget Limit: $100/day per teamId
< 80%: GPT-4
>= 80%: GPT-4o-mini
>= 100%: Error
```
## Version Management
- Save changes as new version
- Test with `model: 'dynamic/route@v2'`
- Roll back by deploying previous version
## Monitoring
Dashboard → Gateway → Dynamic Routes:
- Request count per path
- Success/error rates
- Latency/cost by path
## Limitations
- Max 5 metadata entries
- Values: string/number/boolean/null only
- No nested objects
- Route names: alphanumeric + hyphens

View File

@@ -0,0 +1,96 @@
# Features & Capabilities
## Caching
Dashboard: Settings → Cache Responses → Enable
```typescript
// Custom TTL (1 hour)
headers: { 'cf-aig-cache-ttl': '3600' }
// Skip cache
headers: { 'cf-aig-skip-cache': 'true' }
// Custom cache key
headers: { 'cf-aig-cache-key': 'greeting-en' }
```
**Limits:** TTL 60s - 30 days. **Does NOT work with streaming.**
## Rate Limiting
Dashboard: Settings → Rate-limiting → Enable
- **Fixed window:** Resets at intervals
- **Sliding window:** Rolling window (more accurate)
- Returns `429` when exceeded
## Guardrails
Dashboard: Settings → Guardrails → Enable
Filter prompts/responses for inappropriate content. Actions: Flag (log) or Block (reject).
## Data Loss Prevention (DLP)
Dashboard: Settings → DLP → Enable
Detect PII (emails, SSNs, credit cards). Actions: Flag, Block, or Redact.
## Billing Modes
| Mode | Description | Setup |
|------|-------------|-------|
| **Unified Billing** | Pay through Cloudflare, no provider keys | Use `cf-aig-authorization` header only |
| **BYOK** | Store provider keys in dashboard | Add keys in Provider Keys section |
| **Pass-through** | Send provider key with each request | Include provider's auth header |
## Zero Data Retention
Dashboard: Settings → Privacy → Zero Data Retention
No prompts/responses stored. Request counts and costs still tracked.
## Logging
Dashboard: Settings → Logs → Enable (up to 10M logs)
Each entry: prompt, response, provider, model, tokens, cost, duration, cache status, metadata.
```typescript
// Skip logging for request
headers: { 'cf-aig-collect-log': 'false' }
```
**Export:** Use Logpush to S3, GCS, Datadog, Splunk, etc.
## Custom Cost Tracking
For models not in Cloudflare's pricing database:
Dashboard: Gateway → Settings → Custom Costs
Or via API: set `model`, `input_cost`, `output_cost`.
## Supported Providers (22+)
| Provider | Unified API | Notes |
|----------|-------------|-------|
| OpenAI | `openai/gpt-4o` | Full support |
| Anthropic | `anthropic/claude-sonnet-4-5` | Full support |
| Google AI | `google-ai-studio/gemini-2.0-flash` | Full support |
| Workers AI | `workersai/@cf/meta/llama-3` | Native |
| Azure OpenAI | `azure-openai/*` | Deployment names |
| AWS Bedrock | Provider endpoint only | `/bedrock/*` |
| Groq | `groq/*` | Fast inference |
| Mistral, Cohere, Perplexity, xAI, DeepSeek, Cerebras | Full support | - |
## Best Practices
1. Enable caching for deterministic prompts
2. Set rate limits to prevent abuse
3. Use guardrails for user-facing AI
4. Enable DLP for sensitive data
5. Use unified billing or BYOK for simpler key management
6. Enable logging for debugging
7. Use zero data retention when privacy required

View File

@@ -0,0 +1,114 @@
# AI Gateway SDK Integration
## Vercel AI SDK (Recommended)
```typescript
import { createAiGateway } from 'ai-gateway-provider';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
const gateway = createAiGateway({
accountId: process.env.CF_ACCOUNT_ID,
gateway: process.env.CF_GATEWAY_ID,
apiKey: process.env.CF_API_TOKEN // Optional for auth gateways
});
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Single model
const { text } = await generateText({
model: gateway(openai('gpt-4o')),
prompt: 'Hello'
});
// Automatic fallback array
const { text } = await generateText({
model: gateway([
openai('gpt-4o'),
anthropic('claude-sonnet-4-5'),
openai('gpt-4o-mini')
]),
prompt: 'Complex task'
});
```
### Options
```typescript
model: gateway(openai('gpt-4o'), {
cacheKey: 'my-key',
cacheTtl: 3600,
metadata: { userId: 'u123', team: 'eng' }, // Max 5 entries
retries: { maxAttempts: 3, backoff: 'exponential' }
})
```
## OpenAI SDK
```typescript
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
// Unified API - switch providers via model name
model: 'openai/gpt-4o' // or 'anthropic/claude-sonnet-4-5'
```
## Anthropic SDK
```typescript
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${cfToken}` }
});
```
## Workers AI Binding
```toml
# wrangler.toml
[ai]
binding = "AI"
[[ai.gateway]]
id = "my-gateway"
```
```typescript
await env.AI.run('@cf/meta/llama-3-8b-instruct',
{ messages: [...] },
{ gateway: { id: 'my-gateway', metadata: { userId: '123' } } }
);
```
## LangChain / LlamaIndex
```typescript
// Use OpenAI SDK pattern with custom baseURL
new ChatOpenAI({
configuration: {
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`
}
});
```
## HTTP / cURL
```bash
curl https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/chat/completions \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "cf-aig-authorization: Bearer $CF_TOKEN" \
-H "cf-aig-metadata: {\"userId\":\"123\"}" \
-d '{"model":"gpt-4o","messages":[...]}'
```
## Headers Reference
| Header | Purpose |
|--------|---------|
| `cf-aig-authorization` | Gateway auth token |
| `cf-aig-metadata` | JSON object (max 5 keys) |
| `cf-aig-cache-ttl` | Cache TTL in seconds |
| `cf-aig-skip-cache` | `true` to bypass cache |

View File

@@ -0,0 +1,88 @@
# AI Gateway Troubleshooting
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| 401 | Missing `cf-aig-authorization` header | Add header with CF API token |
| 403 | Invalid provider key / BYOK expired | Check provider key in dashboard |
| 429 | Rate limit exceeded | Increase limit or implement backoff |
### 401 Fix
```typescript
const client = new OpenAI({
baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/openai`,
defaultHeaders: { 'cf-aig-authorization': `Bearer ${CF_API_TOKEN}` }
});
```
### 429 Retry Pattern
```typescript
async function requestWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try { return await fn(); }
catch (e) {
if (e.status === 429 && i < maxRetries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
throw e;
}
}
}
```
## Gotchas
| Issue | Reality |
|-------|---------|
| Metadata limits | Max 5 entries, flat only (no nesting) |
| Cache key collision | Use unique keys per expected response |
| BYOK + Unified Billing | Mutually exclusive |
| Rate limit scope | Per-gateway, not per-user (use dynamic routing for per-user) |
| Log delay | 30-60 seconds normal |
| Streaming + caching | **Incompatible** |
| Model name (unified API) | Prefix required: `openai/gpt-4o`, not `gpt-4o` |
## Cache Not Working
**Causes:**
- Different request params (temperature, etc.)
- Streaming enabled
- Caching disabled in settings
**Check:** `response.headers.get('cf-aig-cache-status')` → HIT or MISS
## Logs Not Appearing
1. Check logging enabled: Dashboard → Gateway → Settings
2. Remove `cf-aig-collect-log: false` header
3. Wait 30-60 seconds
4. Check log limit (10M default)
## Debugging
```bash
# Test connectivity
curl -v https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai/models \
-H "Authorization: Bearer $OPENAI_KEY" \
-H "cf-aig-authorization: Bearer $CF_TOKEN"
```
```typescript
// Check response headers
console.log('Cache:', response.headers.get('cf-aig-cache-status'));
console.log('Request ID:', response.headers.get('cf-ray'));
```
## Analytics
Dashboard → AI Gateway → Select gateway
**Metrics:** Requests, tokens, latency (p50/p95/p99), cache hit rate, costs
**Log filters:** `status: error`, `provider: openai`, `cost > 0.01`, `duration > 1000`
**Export:** Logpush to S3/GCS/Datadog/Splunk

View File

@@ -0,0 +1,138 @@
# Cloudflare AI Search Reference
Expert guidance for implementing Cloudflare AI Search (formerly AutoRAG), Cloudflare's managed semantic search and RAG service.
## Overview
**AI Search** is a managed RAG (Retrieval-Augmented Generation) pipeline that combines:
- Automatic semantic indexing of your content
- Vector similarity search
- Built-in LLM generation
**Key value propositions:**
- **Zero vector management** - No manual embedding, indexing, or storage
- **Auto-indexing** - Content automatically re-indexed every 6 hours
- **Built-in generation** - Optional AI response generation from retrieved context
- **Multi-source** - Index from R2 buckets or website crawls
**Data source options:**
- **R2 bucket** - Index files from Cloudflare R2 (supports MD, TXT, HTML, PDF, DOC, CSV, JSON)
- **Website** - Crawl and index website content (requires Cloudflare-hosted domain)
**Indexing lifecycle:**
- Automatic 6-hour refresh cycle
- Manual "Force Sync" available (30s rate limit)
- Not designed for real-time updates
## Quick Start
**1. Create AI Search instance in dashboard:**
- Go to Cloudflare Dashboard → AI Search → Create
- Choose data source (R2 or website)
- Configure instance name and settings
**2. Configure Worker:**
```jsonc
// wrangler.jsonc
{
"ai": {
"binding": "AI"
}
}
```
**3. Use in Worker:**
```typescript
export default {
async fetch(request, env) {
const answer = await env.AI.autorag("my-search-instance").aiSearch({
query: "How do I configure caching?",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
});
return Response.json({ answer: answer.response });
}
};
```
## When to Use AI Search
### AI Search vs Vectorize
| Factor | AI Search | Vectorize |
|--------|-----------|-----------|
| **Management** | Fully managed | Manual embedding + indexing |
| **Use when** | Want zero-ops RAG pipeline | Need custom embeddings/control |
| **Indexing** | Automatic (6hr cycle) | Manual via API |
| **Generation** | Built-in optional | Bring your own LLM |
| **Data sources** | R2 or website | Manual insert |
| **Best for** | Docs, support, enterprise search | Custom ML pipelines, real-time |
### AI Search vs Direct Workers AI
| Factor | AI Search | Workers AI (direct) |
|--------|-----------|---------------------|
| **Context** | Automatic retrieval | Manual context building |
| **Use when** | Need RAG (search + generate) | Simple generation tasks |
| **Indexing** | Built-in | Not applicable |
| **Best for** | Knowledge bases, docs | Simple chat, transformations |
### search() vs aiSearch()
| Method | Returns | Use When |
|--------|---------|----------|
| `search()` | Search results only | Building custom UI, need raw chunks |
| `aiSearch()` | AI response + results | Need ready-to-use answer (chatbot, Q&A) |
### Real-time Updates Consideration
**AI Search is NOT ideal if:**
- Need real-time content updates (<6 hours)
- Content changes multiple times per hour
- Strict freshness requirements
**AI Search IS ideal if:**
- Content relatively stable (docs, policies, knowledge bases)
- 6-hour refresh acceptable
- Prefer zero-ops over real-time
## Platform Limits
| Limit | Value |
|-------|-------|
| Max instances per account | 10 |
| Max files per instance | 100,000 |
| Max file size | 4 MB |
| Index frequency | Every 6 hours |
| Force Sync rate limit | Once per 30 seconds |
| Filter nesting depth | 2 levels |
| Filters per compound | 10 |
| Score threshold range | 0.0 - 1.0 |
## Reading Order
Navigate these references based on your task:
| Task | Read | Est. Time |
|------|------|-----------|
| **Understand AI Search** | README only | 5 min |
| **Implement basic search** | README → api.md | 10 min |
| **Configure data source** | README → configuration.md | 10 min |
| **Production patterns** | patterns.md | 15 min |
| **Debug issues** | gotchas.md | 10 min |
| **Full implementation** | README → api.md → patterns.md | 30 min |
## In This Reference
- **[api.md](api.md)** - API endpoints, methods, TypeScript interfaces
- **[configuration.md](configuration.md)** - Setup, data sources, wrangler config
- **[patterns.md](patterns.md)** - Common patterns, decision guidance, code examples
- **[gotchas.md](gotchas.md)** - Troubleshooting, code-level gotchas, limits
## See Also
- [Cloudflare AI Search Docs](https://developers.cloudflare.com/ai-search/)
- [Workers AI Docs](https://developers.cloudflare.com/workers-ai/)
- [Vectorize Docs](https://developers.cloudflare.com/vectorize/)

View File

@@ -0,0 +1,87 @@
# AI Search API Reference
## Workers Binding
```typescript
const answer = await env.AI.autorag("instance-name").aiSearch(options);
const results = await env.AI.autorag("instance-name").search(options);
const instances = await env.AI.autorag("_").listInstances();
```
## aiSearch() Options
```typescript
interface AiSearchOptions {
query: string; // User query
model: string; // Workers AI model ID
system_prompt?: string; // LLM instructions
rewrite_query?: boolean; // Fix typos (default: false)
max_num_results?: number; // Max chunks (default: 10)
ranking_options?: { score_threshold?: number }; // 0.0-1.0 (default: 0.3)
reranking?: { enabled: boolean; model: string };
stream?: boolean; // Stream response (default: false)
filters?: Filter; // Metadata filters
page?: string; // Pagination token
}
```
## Response
```typescript
interface AiSearchResponse {
search_query: string; // Query used (rewritten if enabled)
response: string; // AI-generated answer
data: SearchResult[]; // Retrieved chunks
has_more: boolean;
next_page?: string;
}
interface SearchResult {
id: string;
score: number;
content: string;
metadata: { filename: string; folder: string; timestamp: number };
}
```
## Filters
```typescript
// Comparison
{ column: "folder", operator: "gte", value: "docs/" }
// Compound
{ operator: "and", filters: [
{ column: "folder", operator: "gte", value: "docs/" },
{ column: "timestamp", operator: "gte", value: 1704067200 }
]}
```
**Operators:** `eq`, `ne`, `gt`, `gte`, `lt`, `lte`
**Built-in metadata:** `filename`, `folder`, `timestamp` (Unix seconds)
## Streaming
```typescript
const stream = await env.AI.autorag("docs").aiSearch({ query, model, stream: true });
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
```
## Error Types
| Error | Cause |
|-------|-------|
| `AutoRAGNotFoundError` | Instance doesn't exist |
| `AutoRAGUnauthorizedError` | Invalid/missing token |
| `AutoRAGValidationError` | Invalid parameters |
## REST API
```bash
curl https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/autorag/rags/{NAME}/ai-search \
-H "Authorization: Bearer {TOKEN}" \
-d '{"query": "...", "model": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"}'
```
Requires Service API token with "AI Search - Read" permission.

View File

@@ -0,0 +1,88 @@
# AI Search Configuration
## Worker Setup
```jsonc
// wrangler.jsonc
{
"ai": { "binding": "AI" }
}
```
```typescript
interface Env {
AI: Ai;
}
const answer = await env.AI.autorag("my-instance").aiSearch({
query: "How do I configure caching?",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
});
```
## Data Sources
### R2 Bucket
Dashboard: AI Search → Create Instance → Select R2 bucket
**Supported formats:** `.md`, `.txt`, `.html`, `.pdf`, `.doc`, `.docx`, `.csv`, `.json`
**Auto-indexed metadata:** `filename`, `folder`, `timestamp`
### Website Crawler
Requirements:
- Domain on Cloudflare
- `sitemap.xml` at root
- Bot protection must allow `CloudflareAISearch` user agent
## Path Filtering (R2)
```
docs/**/*.md # All .md in docs/ recursively
**/*.draft.md # Exclude (use in exclude patterns)
```
## Indexing
- **Automatic:** Every 6 hours
- **Force Sync:** Dashboard button (30s rate limit between syncs)
- **Pause:** Settings → Pause Indexing (existing index remains searchable)
## Service API Token
Dashboard: AI Search → Instance → Use AI Search → API → Create Token
Permissions:
- **Read** - search operations
- **Edit** - instance management
Store securely:
```bash
wrangler secret put AI_SEARCH_TOKEN
```
## Multi-Environment
```toml
# wrangler.toml
[env.production.vars]
AI_SEARCH_INSTANCE = "prod-docs"
[env.staging.vars]
AI_SEARCH_INSTANCE = "staging-docs"
```
```typescript
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({ query });
```
## Monitoring
```typescript
const instances = await env.AI.autorag("_").listInstances();
console.log(instances.find(i => i.name === "docs"));
```
Dashboard shows: files indexed, status, last index time, storage usage.

View File

@@ -0,0 +1,81 @@
# AI Search Gotchas
## Type Safety
**Timestamp precision:** Use seconds (10-digit), not milliseconds.
```typescript
const nowInSeconds = Math.floor(Date.now() / 1000); // Correct
```
**Folder prefix matching:** Use `gte` for "starts with" on paths.
```typescript
filters: { column: "folder", operator: "gte", value: "docs/api/" } // Matches nested
```
## Filter Limitations
| Limit | Value |
|-------|-------|
| Max nesting depth | 2 levels |
| Filters per compound | 10 |
| `or` operator | Same column, `eq` only |
**OR restriction example:**
```typescript
// ✅ Valid: same column, eq only
{ operator: "or", filters: [
{ column: "folder", operator: "eq", value: "docs/" },
{ column: "folder", operator: "eq", value: "guides/" }
]}
```
## Indexing Issues
| Problem | Cause | Solution |
|---------|-------|----------|
| File not indexed | Unsupported format or >4MB | Check format (.md/.txt/.html/.pdf/.doc/.csv/.json) |
| Index out of sync | 6-hour index cycle | Wait or use "Force Sync" (30s rate limit) |
| Empty results | Index incomplete | Check dashboard for indexing status |
## Auth Errors
| Error | Cause | Fix |
|-------|-------|-----|
| `AutoRAGUnauthorizedError` | Invalid/missing token | Create Service API token with AI Search permissions |
| `AutoRAGNotFoundError` | Wrong instance name | Verify exact name from dashboard |
## Performance
**Slow responses (>3s):**
```typescript
// Add score threshold + limit results
ranking_options: { score_threshold: 0.5 },
max_num_results: 10
```
**Empty results debug:**
1. Remove filters, test basic query
2. Lower `score_threshold` to 0.1
3. Check index is populated
## Limits
| Resource | Limit |
|----------|-------|
| Instances per account | 10 |
| Files per instance | 100,000 |
| Max file size | 4 MB |
| Index frequency | 6 hours |
## Anti-Patterns
**Use env vars for instance names:**
```typescript
const answer = await env.AI.autorag(env.AI_SEARCH_INSTANCE).aiSearch({...});
```
**Handle specific error types:**
```typescript
if (error instanceof AutoRAGNotFoundError) { /* 404 */ }
if (error instanceof AutoRAGUnauthorizedError) { /* 401 */ }
```

View File

@@ -0,0 +1,85 @@
# AI Search Patterns
## search() vs aiSearch()
| Use | Method | Returns |
|-----|--------|---------|
| Custom UI, analytics | `search()` | Raw chunks only (~100-300ms) |
| Chatbots, Q&A | `aiSearch()` | AI response + chunks (~500-2000ms) |
## rewrite_query
| Setting | Use When |
|---------|----------|
| `true` | User input (typos, vague queries) |
| `false` | LLM-generated queries (already optimized) |
## Multitenancy (Folder-Based)
```typescript
const answer = await env.AI.autorag("saas-docs").aiSearch({
query: "refund policy",
model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
filters: {
column: "folder",
operator: "gte", // "starts with" pattern
value: `tenants/${tenantId}/`
}
});
```
## Streaming
```typescript
const stream = await env.AI.autorag("docs").aiSearch({
query, model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", stream: true
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
```
## Score Threshold
| Threshold | Use |
|-----------|-----|
| 0.3 (default) | Broad recall, exploratory |
| 0.5 | Balanced, production default |
| 0.7 | High precision, critical accuracy |
## System Prompt Template
```typescript
const systemPrompt = `You are a documentation assistant.
- Answer ONLY based on provided context
- If context doesn't contain answer, say "I don't have information"
- Include code examples from context`;
```
## Compound Filters
```typescript
// OR: Multiple folders
filters: {
operator: "or",
filters: [
{ column: "folder", operator: "gte", value: "docs/api/" },
{ column: "folder", operator: "gte", value: "docs/auth/" }
]
}
// AND: Folder + date
filters: {
operator: "and",
filters: [
{ column: "folder", operator: "gte", value: "docs/" },
{ column: "timestamp", operator: "gte", value: oneWeekAgoSeconds }
]
}
```
## Reranking
Enable for high-stakes use cases (adds ~300ms latency):
```typescript
reranking: { enabled: true, model: "@cf/baai/bge-reranker-base" }
```

View File

@@ -0,0 +1,92 @@
# Cloudflare Workers Analytics Engine Reference
Expert guidance for implementing unlimited-cardinality analytics at scale using Cloudflare Workers Analytics Engine.
## What is Analytics Engine?
Time-series analytics database designed for high-cardinality data (millions of unique dimensions). Write data points from Workers, query via SQL API. Use for:
- Custom user-facing analytics dashboards
- Usage-based billing & metering
- Per-customer/per-feature monitoring
- High-frequency instrumentation without performance impact
**Key Capability:** Track metrics with unlimited unique values (e.g., millions of user IDs, API keys) without performance degradation.
## Core Concepts
| Concept | Description | Example |
|---------|-------------|---------|
| **Dataset** | Logical table for related metrics | `api_requests`, `user_events` |
| **Data Point** | Single measurement with timestamp | One API request's metrics |
| **Blobs** | String dimensions (max 20) | endpoint, method, status, user_id |
| **Doubles** | Numeric values (max 20) | latency_ms, request_count, bytes |
| **Indexes** | Filtered blobs for efficient queries | customer_id, api_key |
## Reading Order
| Task | Start Here | Then Read |
|------|------------|-----------|
| **First-time setup** | [configuration.md](configuration.md) → [api.md](api.md) → [patterns.md](patterns.md) | |
| **Writing data** | [api.md](api.md) → [gotchas.md](gotchas.md) (sampling) | |
| **Querying data** | [api.md](api.md) (SQL API) → [patterns.md](patterns.md) (examples) | |
| **Debugging** | [gotchas.md](gotchas.md) → [api.md](api.md) (limits) | |
| **Optimization** | [patterns.md](patterns.md) (anti-patterns) → [gotchas.md](gotchas.md) | |
## When to Use Analytics Engine
```
Need to track metrics? → Yes
Millions of unique dimension values? → Yes
Need real-time queries? → Yes
Use Analytics Engine ✓
Alternative scenarios:
- Low cardinality (<10k unique values) → Workers Analytics (free tier)
- Complex joins/relations → D1 Database
- Logs/debugging → Tail Workers (logpush)
- External tools → Send to external analytics (Datadog, etc.)
```
## Quick Start
1. Add binding to `wrangler.jsonc`:
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "my_events" }
]
}
```
2. Write data points (fire-and-forget, no await):
```typescript
env.ANALYTICS.writeDataPoint({
blobs: ["/api/users", "GET", "200"],
doubles: [145.2, 1], // latency_ms, count
indexes: [customerId]
});
```
3. Query via SQL API (HTTP):
```sql
SELECT blob1, SUM(double2) AS total_requests
FROM my_events
WHERE index1 = 'customer_123'
AND timestamp >= NOW() - INTERVAL '7' DAY
GROUP BY blob1
ORDER BY total_requests DESC
```
## In This Reference
- **[configuration.md](configuration.md)** - Setup, bindings, TypeScript types, limits
- **[api.md](api.md)** - `writeDataPoint()`, SQL API, query syntax
- **[patterns.md](patterns.md)** - Use cases, examples, anti-patterns
- **[gotchas.md](gotchas.md)** - Sampling, index selection, troubleshooting
## See Also
- [Cloudflare Analytics Engine Docs](https://developers.cloudflare.com/analytics/analytics-engine/)

View File

@@ -0,0 +1,112 @@
# Analytics Engine API Reference
## Writing Data
### `writeDataPoint()`
Fire-and-forget (returns `void`, not Promise). Writes happen asynchronously.
```typescript
interface AnalyticsEngineDataPoint {
blobs?: string[]; // Up to 20 strings (dimensions), 16KB each
doubles?: number[]; // Up to 20 numbers (metrics)
indexes?: string[]; // 1 indexed string for high-cardinality filtering
}
env.ANALYTICS.writeDataPoint({
blobs: ["/api/users", "GET", "200"],
doubles: [145.2, 1], // latency_ms, count
indexes: ["customer_abc123"]
});
```
**Behaviors:** No await needed, no error thrown (check tail logs), auto-sampled at high volumes, auto-timestamped.
**Blob vs Index:** Blob for GROUP BY (<100k unique), Index for filter-only (millions unique).
### Full Example
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const start = Date.now();
const url = new URL(request.url);
try {
const response = await handleRequest(request);
env.ANALYTICS.writeDataPoint({
blobs: [url.pathname, request.method, response.status.toString()],
doubles: [Date.now() - start, 1],
indexes: [request.headers.get("x-api-key") || "anonymous"]
});
return response;
} catch (error) {
env.ANALYTICS.writeDataPoint({
blobs: [url.pathname, request.method, "500"],
doubles: [Date.now() - start, 1, 0],
});
throw error;
}
}
};
```
## SQL API (External Only)
```bash
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics_engine/sql \
-H "Authorization: Bearer $TOKEN" \
-d "SELECT blob1 AS endpoint, COUNT(*) AS requests FROM dataset WHERE timestamp >= NOW() - INTERVAL '1' HOUR GROUP BY blob1"
```
### Column References
```sql
-- blob1..blob20, double1..double20, index1, timestamp
SELECT blob1 AS endpoint, SUM(double1) AS latency, COUNT(*) AS requests
FROM my_dataset
WHERE index1 = 'customer_123' AND timestamp >= NOW() - INTERVAL '7' DAY
GROUP BY blob1
HAVING COUNT(*) > 100
ORDER BY requests DESC LIMIT 100
```
**Aggregations:** `SUM()`, `AVG()`, `COUNT()`, `MIN()`, `MAX()`, `quantile(0.95)()`
**Time ranges:** `NOW() - INTERVAL '1' HOUR`, `BETWEEN '2026-01-01' AND '2026-01-31'`
### Query Examples
```sql
-- Top endpoints
SELECT blob1, COUNT(*) AS requests, AVG(double1) AS avg_latency
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '24' HOUR
GROUP BY blob1 ORDER BY requests DESC LIMIT 20
-- Error rate
SELECT blob1, COUNT(*) AS total,
SUM(CASE WHEN blob3 LIKE '5%' THEN 1 ELSE 0 END) AS errors
FROM api_requests WHERE timestamp >= NOW() - INTERVAL '1' HOUR
GROUP BY blob1 HAVING total > 50
-- P95 latency
SELECT blob1, quantile(0.95)(double1) AS p95
FROM api_requests GROUP BY blob1
```
## Response Format
```json
{"data": [{"endpoint": "/api/users", "requests": 1523}], "rows": 2}
```
## Limits
| Resource | Limit |
|----------|-------|
| Blobs/Doubles per point | 20 each |
| Indexes per point | 1 |
| Blob/Index size | 16KB |
| Data retention | 90 days |
| Query timeout | 30s |
**Critical:** High write volumes (>1M/min) trigger automatic sampling.

View File

@@ -0,0 +1,112 @@
# Analytics Engine Configuration
## Setup
1. Add binding to `wrangler.jsonc`
2. Deploy Worker
3. Dataset created automatically on first write
4. Query via SQL API
## wrangler.jsonc
```jsonc
{
"name": "my-worker",
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "my_events" }
]
}
```
Multiple datasets for separate concerns:
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "API_ANALYTICS", "dataset": "api_requests" },
{ "binding": "USER_EVENTS", "dataset": "user_activity" }
]
}
```
## TypeScript
```typescript
interface Env {
ANALYTICS: AnalyticsEngineDataset;
}
export default {
async fetch(request: Request, env: Env) {
// No await - returns void, fire-and-forget
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, status], // String dimensions (max 20)
doubles: [latency, 1], // Numeric metrics (max 20)
indexes: [apiKey] // High-cardinality filter (max 1)
});
return response;
}
};
```
## Data Point Limits
| Field | Limit | SQL Access |
|-------|-------|------------|
| blobs | 20 strings, 16KB each | `blob1`...`blob20` |
| doubles | 20 numbers | `double1`...`double20` |
| indexes | 1 string, 16KB | `index1` |
## Write Behavior
| Scenario | Behavior |
|----------|----------|
| <1M writes/min | All accepted |
| >1M writes/min | Automatic sampling |
| Invalid data | Silent failure (check tail logs) |
**Mitigate sampling:** Pre-aggregate, use multiple datasets, write only critical metrics.
## Query Limits
| Resource | Limit |
|----------|-------|
| Query timeout | 30 seconds |
| Data retention | 90 days (default) |
| Result size | ~10MB |
## Cost
**Free tier:** 10M writes/month, 1M reads/month
**Paid:** $0.05 per 1M writes, $1.00 per 1M reads
## Environment-Specific
```jsonc
{
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "prod_events" }
],
"env": {
"staging": {
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "staging_events" }
]
}
}
}
```
## Monitoring
```bash
npx wrangler tail # Check for sampling/write errors
```
```sql
-- Check write activity
SELECT DATE_TRUNC('hour', timestamp) AS hour, COUNT(*) AS writes
FROM my_dataset
WHERE timestamp >= NOW() - INTERVAL '24' HOUR
GROUP BY hour
```

View File

@@ -0,0 +1,85 @@
# Analytics Engine Gotchas
## Critical Issues
### Sampling at High Volumes
**Problem:** Queries return fewer points than written at >1M writes/min.
**Solution:**
```typescript
// Pre-aggregate before writing
let buffer = { count: 0, total: 0 };
buffer.count++; buffer.total += value;
// Write once per second instead of per request
if (Date.now() % 1000 === 0) {
env.ANALYTICS.writeDataPoint({ doubles: [buffer.count, buffer.total] });
}
```
**Detection:** `npx wrangler tail` → look for "sampling enabled"
### writeDataPoint Returns void
```typescript
// ❌ Pointless await
await env.ANALYTICS.writeDataPoint({...});
// ✅ Fire-and-forget
env.ANALYTICS.writeDataPoint({...});
```
Writes can fail silently. Check tail logs.
### Index vs Blob
| Cardinality | Use | Example |
|-------------|-----|---------|
| Millions | **Index** | user_id, api_key |
| Hundreds | **Blob** | endpoint, status_code, country |
```typescript
// ✅ Correct
{ blobs: [method, path, status], indexes: [userId] }
```
### Can't Query from Workers
Query API requires HTTP auth. Use external service or cache in KV/D1.
### No Custom Timestamps
Auto-generated at write time. Store original in blob if needed.
## Common Errors
| Error | Fix |
|-------|-----|
| Binding not found | Check wrangler.jsonc, redeploy |
| No data in query | Wait 30s; check dataset name; check time range |
| Query timeout | Add time filter; use index for filtering |
## Limits
| Resource | Limit |
|----------|-------|
| Blobs per point | 20 |
| Doubles per point | 20 |
| Indexes per point | 1 |
| Blob/Index size | 16KB |
| Write rate (no sampling) | ~1M/min |
| Retention | 90 days |
| Query timeout | 30s |
## Best Practices
✅ Pre-aggregate at high volumes
✅ Use index for high-cardinality (millions)
✅ Always include time filter in queries
✅ Design schema before coding
❌ Don't await writeDataPoint
❌ Don't use index for low-cardinality
❌ Don't query without time range
❌ Don't assume all writes succeed

View File

@@ -0,0 +1,83 @@
# Analytics Engine Patterns
## Use Cases
| Use Case | Key Metrics | Index On |
|----------|-------------|----------|
| API Metering | requests, bytes, compute_units | api_key |
| Feature Usage | feature, action, duration | user_id |
| Error Tracking | error_type, endpoint, count | customer_id |
| Performance | latency_ms, cache_status | endpoint |
| A/B Testing | variant, conversions | user_id |
## API Metering (Billing)
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, status, tier],
doubles: [1, computeUnits, bytes, latencyMs],
indexes: [apiKey]
});
// Query: Monthly usage by customer
// SELECT index1 AS api_key, SUM(double2) AS compute_units
// FROM usage WHERE timestamp >= DATE_TRUNC('month', NOW()) GROUP BY index1
```
## Error Tracking
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [endpoint, method, errorName, errorMessage.slice(0, 1000)],
doubles: [1, timeToErrorMs],
indexes: [customerId]
});
```
## Performance Monitoring
```typescript
env.ANALYTICS.writeDataPoint({
blobs: [pathname, method, cacheStatus, status],
doubles: [latencyMs, 1],
indexes: [userId]
});
// Query: P95 latency by endpoint
// SELECT blob1, quantile(0.95)(double1) AS p95_ms FROM perf GROUP BY blob1
```
## Anti-Patterns
| ❌ Wrong | ✅ Correct |
|----------|-----------|
| `await writeDataPoint()` | `writeDataPoint()` (fire-and-forget) |
| `indexes: [method]` (low cardinality) | `blobs: [method]`, `indexes: [userId]` |
| `blobs: [JSON.stringify(obj)]` | Store ID in blob, full object in D1/KV |
| Write every request at 10M/min | Pre-aggregate per second |
| Query from Worker | Query from external service/API |
## Best Practices
1. **Design schema upfront** - Document blob/double/index assignments
2. **Always include count metric** - `doubles: [latency, 1]` for AVG calculations
3. **Use enums for blobs** - Consistent values like `Status.SUCCESS`
4. **Handle sampling** - Use ratios (avg_latency = SUM(latency)/SUM(count))
5. **Test queries early** - Validate schema before heavy writes
## Schema Template
```typescript
/**
* Dataset: my_metrics
*
* Blobs:
* blob1: endpoint, blob2: method, blob3: status
*
* Doubles:
* double1: latency_ms, double2: count (always 1)
*
* Indexes:
* index1: customer_id (high cardinality)
*/
```

View File

@@ -0,0 +1,44 @@
# Cloudflare API Shield Reference
Expert guidance for API Shield - comprehensive API security suite for discovery, protection, and monitoring.
## Reading Order
| Task | Files to Read |
|------|---------------|
| Initial setup | README → configuration.md |
| Implement JWT validation | configuration.md → api.md |
| Add schema validation | configuration.md → patterns.md |
| Detect API attacks | patterns.md → api.md |
| Debug issues | gotchas.md |
## Feature Selection
What protection do you need?
```
├─ Validate request/response structure → Schema Validation 2.0 (configuration.md)
├─ Verify auth tokens → JWT Validation (configuration.md)
├─ Client certificates → mTLS (configuration.md)
├─ Detect BOLA attacks → BOLA Detection (patterns.md)
├─ Track auth coverage → Auth Posture (patterns.md)
├─ Stop volumetric abuse → Abuse Detection (patterns.md)
└─ Discover shadow APIs → API Discovery (api.md)
```
## In This Reference
- **[configuration.md](configuration.md)** - Setup, session identifiers, rules, token/mTLS configs
- **[api.md](api.md)** - Endpoint management, discovery, validation APIs, GraphQL operations
- **[patterns.md](patterns.md)** - Common patterns, progressive rollout, OWASP mappings, workflows
- **[gotchas.md](gotchas.md)** - Troubleshooting, false positives, performance, best practices
## Quick Start
API Shield: Enterprise-grade API security (Discovery, Schema Validation 2.0, JWT, mTLS, BOLA Detection, Auth Posture). Available as Enterprise add-on with preview access.
## See Also
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)
- [API Reference](https://developers.cloudflare.com/api/resources/api_gateway/)
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)

View File

@@ -0,0 +1,141 @@
# API Reference
Base: `/zones/{zone_id}/api_gateway`
## Endpoints
```bash
GET /operations # List
GET /operations/{op_id} # Get single
POST /operations/item # Create: {endpoint,host,method}
POST /operations # Bulk: {operations:[{endpoint,host,method}]}
DELETE /operations/{op_id} # Delete
DELETE /operations # Bulk delete: {operation_ids:[...]}
```
## Discovery
```bash
GET /discovery/operations # List discovered
PATCH /discovery/operations/{op_id} # Update: {state:"saved"|"ignored"}
PATCH /discovery/operations # Bulk: {operation_ids:{id:{state}}}
GET /discovery # OpenAPI export
```
## Config
```bash
GET /configuration # Get session ID config
PUT /configuration # Update: {auth_id_characteristics:[{name,type:"header"|"cookie"}]}
```
## Token Validation
```bash
GET /token_validation # List
POST /token_validation # Create: {name,location:{header:"..."},jwks:"..."}
POST /jwt_validation_rules # Rule: {name,hostname,token_validation_id,action:"block"}
```
## Workers Integration
### Access JWT Claims
```js
export default {
async fetch(req, env) {
// Access validated JWT payload
const jwt = req.cf?.jwt?.payload?.[env.JWT_CONFIG_ID]?.[0];
if (jwt) {
const userId = jwt.sub;
const role = jwt.role;
}
}
}
```
### Access mTLS Info
```js
export default {
async fetch(req, env) {
const tls = req.cf?.tlsClientAuth;
if (tls?.certVerified === 'SUCCESS') {
const fingerprint = tls.certFingerprintSHA256;
// Authenticated client
}
}
}
```
### Dynamic JWKS Update
```js
export default {
async scheduled(event, env) {
const jwks = await (await fetch('https://auth.example.com/.well-known/jwks.json')).json();
await fetch(`https://api.cloudflare.com/client/v4/zones/${env.ZONE_ID}/api_gateway/token_validation/${env.CONFIG_ID}`, {
method: 'PATCH',
headers: {'Authorization': `Bearer ${env.CF_API_TOKEN}`, 'Content-Type': 'application/json'},
body: JSON.stringify({jwks: JSON.stringify(jwks)})
});
}
}
```
## Firewall Fields
### Core Fields
```js
cf.api_gateway.auth_id_present // Session ID present
cf.api_gateway.request_violates_schema // Schema violation
cf.api_gateway.fallthrough_triggered // No endpoint match
cf.tls_client_auth.cert_verified // mTLS cert valid
cf.tls_client_auth.cert_fingerprint_sha256
```
### JWT Validation (2026)
```js
// Modern validation syntax
is_jwt_valid(http.request.jwt.payload["{config_id}"][0])
// Legacy (still supported)
cf.api_gateway.jwt_claims_valid
// Extract claims
lookup_json_string(http.request.jwt.payload["{config_id}"][0], "claim_name")
```
### Risk Labels (2026)
```js
// BOLA detection
cf.api_gateway.cf-risk-bola-enumeration // Sequential resource access detected
cf.api_gateway.cf-risk-bola-pollution // Parameter pollution detected
// Authentication posture
cf.api_gateway.cf-risk-missing-auth // Endpoint lacks authentication
cf.api_gateway.cf-risk-mixed-auth // Inconsistent auth patterns
```
## BOLA Detection
```bash
GET /user_schemas/{schema_id}/bola # Get BOLA config
PATCH /user_schemas/{schema_id}/bola # Update: {enabled:true}
```
## Auth Posture
```bash
GET /discovery/authentication_posture # List unprotected endpoints
```
## GraphQL Protection
```bash
GET /settings/graphql_protection # Get limits
PUT /settings/graphql_protection # Set: {max_depth,max_size}
```
## See Also
- [configuration.md](configuration.md) - Setup guides for all features
- [patterns.md](patterns.md) - Firewall rules and common patterns
- [API Gateway API Docs](https://developers.cloudflare.com/api/resources/api_gateway/)

View File

@@ -0,0 +1,192 @@
# Configuration
## Schema Validation 2.0 Setup
> ⚠️ **Classic Schema Validation deprecated.** Use Schema Validation 2.0.
**Upload schema (Dashboard):**
```
Security > API Shield > Schema Validation > Add validation
- Upload .yml/.yaml/.json (OpenAPI v3.0)
- Endpoints auto-added to Endpoint Management
- Action: Log | Block | None
- Body inspection: JSON payloads
```
**Change validation action:**
```
Security > API Shield > Settings > Schema Validation
Per-endpoint: Filter → ellipses → Change action
Default action: Set global mitigation action
```
**Migration from Classic:**
```
1. Export existing schema (if available)
2. Delete all Classic schema validation rules
3. Wait 5 min for cache clear
4. Re-upload via Schema Validation 2.0 interface
5. Verify in Security > Events
```
**Fallthrough rule** (catch-all unknown endpoints):
```
Security > API Shield > Settings > Fallthrough > Use Template
- Select hostnames
- Create rule with cf.api_gateway.fallthrough_triggered
- Action: Log (discover) or Block (strict)
```
**Body inspection:** Supports `application/json`, `*/*`, `application/*`. Disable origin MIME sniffing to prevent bypasses.
## JWT Validation
**Setup token config:**
```
Security > API Shield > Settings > JWT Settings > Add configuration
- Name: "Auth0 JWT Config"
- Location: Header/Cookie + name (e.g., "Authorization")
- JWKS: Paste public keys from IdP
```
**Create validation rule:**
```
Security > API Shield > API Rules > Add rule
- Hostname: api.example.com
- Deselect endpoints to ignore
- Token config: Select config
- Enforce presence: Ignore or Mark as non-compliant
- Action: Log/Block/Challenge
```
**Rate limit by JWT claim:**
```wirefilter
lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
```
**Special cases:**
- Two JWTs, different IdPs: Create 2 configs, select both, "Validate all"
- IdP migration: 2 configs + 2 rules, adjust actions per state
- Bearer prefix: API Shield handles with/without
- Nested claims: Dot notation `user.email`
## Mutual TLS (mTLS)
**Setup:**
```
SSL/TLS > Client Certificates > Create Certificate
- Generate CF-managed CA (all plans)
- Upload custom CA (Enterprise, max 5)
```
**Configure mTLS rule:**
```
Security > API Shield > mTLS
- Select hostname(s)
- Choose certificate(s)
- Action: Block/Log/Challenge
```
**Test:**
```bash
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365
curl https://api.example.com/endpoint --cert client-cert.pem --key client-key.pem
```
## Session Identifiers
Critical for BOLA Detection, Sequence Mitigation, and analytics. Configure header/cookie that uniquely IDs API users.
**Examples:** JWT sub claim, session token, API key, custom user ID header
**Configure:**
```
Security > API Shield > Settings > Session Identifiers
- Type: Header/Cookie
- Name: "X-User-ID" or "Authorization"
```
## BOLA Detection
Detects Broken Object Level Authorization attacks (enumeration + parameter pollution).
**Enable:**
```
Security > API Shield > Schema Validation > [Select Schema] > BOLA Detection
- Enable detection
- Threshold: Sensitivity level (Low/Medium/High)
- Action: Log or Block
```
**Requirements:**
- Schema Validation 2.0 enabled
- Session identifiers configured
- Minimum traffic: 1000+ requests/day per endpoint
## Authentication Posture
Identifies unprotected or inconsistently protected endpoints.
**View report:**
```
Security > API Shield > Authentication Posture
- Shows endpoints lacking JWT/mTLS
- Highlights mixed authentication patterns
```
**Remediate:**
1. Review flagged endpoints
2. Add JWT validation rules
3. Configure mTLS for sensitive endpoints
4. Monitor posture score
## Volumetric Abuse + GraphQL
**Volumetric Abuse Detection:**
`Security > API Shield > Settings > Volumetric Abuse Detection`
- Enable per-endpoint monitoring, set thresholds, action: Log | Challenge | Block
**GraphQL Protection:**
`Security > API Shield > Settings > GraphQL Protection`
- Max query depth: 10, max size: 100KB, block introspection (production)
## Terraform
```hcl
# Session identifier
resource "cloudflare_api_shield" "main" {
zone_id = var.zone_id
auth_id_characteristics {
type = "header"
name = "Authorization"
}
}
# Add endpoint
resource "cloudflare_api_shield_operation" "users_get" {
zone_id = var.zone_id
method = "GET"
host = "api.example.com"
endpoint = "/api/users/{id}"
}
# JWT validation rule
resource "cloudflare_ruleset" "jwt_validation" {
zone_id = var.zone_id
name = "API JWT Validation"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = "(http.host eq \"api.example.com\" and not is_jwt_valid(http.request.jwt.payload[\"{config_id}\"][0]))"
description = "Block invalid JWTs"
}
}
```
## See Also
- [api.md](api.md) - API endpoints and Workers integration
- [patterns.md](patterns.md) - Firewall rules and deployment patterns
- [gotchas.md](gotchas.md) - Troubleshooting and limits

View File

@@ -0,0 +1,125 @@
# Gotchas & Troubleshooting
## Common Errors
### "Schema Validation 2.0 not working after migration"
**Cause:** Classic rules still active, conflicting with new system
**Solution:**
1. Delete ALL Classic schema validation rules
2. Clear Cloudflare cache (wait 5 min)
3. Re-upload schema via new Schema Validation 2.0 interface
4. Verify in Security > Events
5. Check action is set (Log/Block)
### "Schema validation blocking valid requests"
**Cause:** Schema too restrictive, missing fields, or incorrect types
**Solution:**
1. Check Firewall Events for violation details
2. Review schema in Settings
3. Test schema in Swagger Editor
4. Use Log mode to validate before blocking
5. Update schema with correct specifications
6. Ensure Schema Validation 2.0 (not Classic)
### "JWT validation failing"
**Cause:** JWKS mismatch with IdP, expired token, wrong header/cookie name, or clock skew
**Solution:**
1. Verify JWKS matches IdP configuration
2. Check token `exp` claim is valid
3. Confirm header/cookie name matches config
4. Test token at jwt.io
5. Account for clock skew (±5 min tolerance)
6. Use modern syntax: `is_jwt_valid(http.request.jwt.payload["{config_id}"][0])`
### "BOLA detection false positives"
**Cause:** Legitimate sequential access patterns, bulk operations, or sensitivity too high
**Solution:**
1. Review BOLA events in Security > Events
2. Lower sensitivity threshold (High → Medium → Low)
3. Exclude legitimate bulk operations from detection
4. Ensure session identifiers uniquely identify users
5. Verify minimum traffic requirements met (1000+ req/day)
### "Risk labels not appearing in firewall rules"
**Cause:** Feature not enabled, insufficient traffic, or missing session identifiers
**Solution:**
1. Verify Schema Validation 2.0 enabled
2. Enable BOLA Detection in schema settings
3. Configure session identifiers (required for BOLA)
4. Wait 24-48h for ML model training
5. Check minimum traffic thresholds met
### "Endpoint discovery not finding APIs"
**Cause:** Insufficient traffic (<500 reqs/10d), non-2xx responses, Worker direct requests, or incorrect session ID config
**Solution:** Ensure 500+ requests in 10 days, 2xx responses from edge (not Workers direct), configure session IDs correctly. ML updates daily.
### "Sequence detection false positives"
**Cause:** Lookback window issues, non-unique session IDs, or model sensitivity
**Solution:**
1. Review lookback settings (10 reqs to managed endpoints, 10min window)
2. Ensure session ID uniqueness per user (not shared tokens)
3. Adjust positive/negative model balance
4. Exclude legitimate workflows from detection
### "GraphQL protection blocking valid queries"
**Cause:** Query depth/size limits too restrictive, complex but legitimate queries
**Solution:**
1. Review blocked query patterns in Security > Events
2. Increase max_depth (default: 10) if needed
3. Increase max_size (default: 100KB) for complex queries
4. Whitelist specific query signatures
5. Use Log mode to tune before blocking
### "Token invalid"
**Cause:** Configuration error, JWKS mismatch, or expired token
**Solution:** Verify config matches IdP, update JWKS, check token expiration
### "Schema violation"
**Cause:** Missing required fields, wrong data types, or spec mismatch
**Solution:** Review schema against actual requests, ensure all required fields present, validate types match spec
### "Fallthrough"
**Cause:** Unknown endpoint or pattern mismatch
**Solution:** Update schema with all endpoints, check path pattern matching
### "mTLS failed"
**Cause:** Certificate untrusted/expired or wrong CA
**Solution:** Verify cert chain, check expiration, confirm correct CA uploaded
## Limits (2026)
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| OpenAPI version | v3.0.x only | No external refs, must be valid |
| Schema operations | 10K (Enterprise) | Contact for higher limits |
| JWT validation sources | Headers/cookies only | No query params/body |
| Endpoint discovery | 500+ reqs/10d | Minimum for ML model |
| Path normalization | Automatic | `/profile/238``/profile/{var1}` |
| Schema parameters | No `content` field | No object param validation |
| BOLA detection | 1000+ reqs/day/endpoint | Per-endpoint minimum |
| Session ID uniqueness | Required | BOLA/Sequence need unique IDs |
| GraphQL max depth | 1-50 | Default: 10 |
| GraphQL max size | 1KB-1MB | Default: 100KB |
| JWT claim nesting | 10 levels max | Use dot notation |
| mTLS CA certificates | 5 custom max | CF-managed unlimited |
| Schema upload size | 5MB max | Compressed OpenAPI spec |
| Volumetric abuse baseline | 7 days training | Initial ML period |
| Auth Posture refresh | Daily | Updated nightly |
## See Also
- [configuration.md](configuration.md) - Setup guides to avoid common issues
- [patterns.md](patterns.md) - Best practices and progressive rollout
- [API Shield Docs](https://developers.cloudflare.com/api-shield/)

View File

@@ -0,0 +1,180 @@
# Patterns & Use Cases
## Protect API with Schema + JWT
```bash
# 1. Upload OpenAPI schema
POST /zones/{zone_id}/api_gateway/user_schemas
# 2. Configure JWT validation
POST /zones/{zone_id}/api_gateway/token_validation
{
"name": "Auth0",
"location": {"header": "Authorization"},
"jwks": "{...}"
}
# 3. Create JWT rule
POST /zones/{zone_id}/api_gateway/jwt_validation_rules
# 4. Set schema validation action
PUT /zones/{zone_id}/api_gateway/settings/schema_validation
{"validation_default_mitigation_action": "block"}
```
## Progressive Rollout
```
1. Log mode: Observe false positives
- Schema: Action = Log
- JWT: Action = Log
2. Block subset: Protect critical endpoints
- Change specific endpoint actions to Block
- Monitor firewall events
3. Full enforcement: Block all violations
- Change default action to Block
- Handle fallthrough with custom rule
```
## BOLA Detection
### Enumeration Detection
Detects sequential resource access (e.g., `/users/1`, `/users/2`, `/users/3`).
```javascript
// Block BOLA enumeration attempts
(cf.api_gateway.cf-risk-bola-enumeration and http.host eq "api.example.com")
// Action: Block or Challenge
```
### Parameter Pollution
Detects duplicate/excessive parameters in requests.
```javascript
// Block parameter pollution
(cf.api_gateway.cf-risk-bola-pollution and http.host eq "api.example.com")
// Action: Block
```
### Combined BOLA Protection
```javascript
// Comprehensive BOLA rule
(cf.api_gateway.cf-risk-bola-enumeration or cf.api_gateway.cf-risk-bola-pollution)
and http.host eq "api.example.com"
// Action: Block
```
## Authentication Posture
### Detect Missing Auth
```javascript
// Log endpoints lacking authentication
(cf.api_gateway.cf-risk-missing-auth and http.host eq "api.example.com")
// Action: Log (for audit)
```
### Detect Mixed Auth
```javascript
// Alert on inconsistent auth patterns
(cf.api_gateway.cf-risk-mixed-auth and http.host eq "api.example.com")
// Action: Log (review required)
```
## Fallthrough Detection (Shadow APIs)
```javascript
// WAF Custom Rule
(cf.api_gateway.fallthrough_triggered and http.host eq "api.example.com")
// Action: Log (discover unknown) or Block (strict)
```
## Rate Limiting by User
```javascript
// Rate Limiting Rule (modern syntax)
(http.host eq "api.example.com" and
is_jwt_valid(http.request.jwt.payload["{config_id}"][0]))
// Rate: 100 req/60s
// Counting expression: lookup_json_string(http.request.jwt.payload["{config_id}"][0], "sub")
```
## Volumetric Abuse Response
```javascript
// Detect abnormal traffic spikes
(cf.api_gateway.volumetric_abuse_detected and http.host eq "api.example.com")
// Action: Challenge or Rate Limit
// Combined with rate limiting
(cf.api_gateway.volumetric_abuse_detected or
cf.threat_score gt 50) and http.host eq "api.example.com"
// Action: JS Challenge
```
## GraphQL Protection
```javascript
// Block oversized queries
(http.request.uri.path eq "/graphql" and
cf.api_gateway.graphql_query_size gt 100000)
// Action: Block
// Block deep nested queries
(http.request.uri.path eq "/graphql" and
cf.api_gateway.graphql_query_depth gt 10)
// Action: Block
```
## Architecture Patterns
**Public API:** Discovery + Schema Validation 2.0 + JWT + Rate Limiting + Bot Management
**Partner API:** mTLS + Schema Validation + Sequence Mitigation
**Internal API:** Discovery + Schema Learning + Auth Posture
## OWASP API Security Top 10 Mapping (2026)
| OWASP Issue | API Shield Solutions |
|-------------|---------------------|
| API1:2023 Broken Object Level Authorization | **BOLA Detection** (enumeration + pollution), Sequence mitigation, Schema, JWT, Rate Limiting |
| API2:2023 Broken Authentication | **Auth Posture**, mTLS, JWT validation, Bot Management |
| API3:2023 Broken Object Property Auth | Schema validation, JWT validation |
| API4:2023 Unrestricted Resource Access | Rate Limiting, **Volumetric Abuse Detection**, **GraphQL Protection**, Bot Management |
| API5:2023 Broken Function Level Auth | Schema validation, JWT validation, Auth Posture |
| API6:2023 Unrestricted Business Flows | Sequence mitigation, Bot Management |
| API7:2023 SSRF | Schema validation, WAF managed rules |
| API8:2023 Security Misconfiguration | **Schema Validation 2.0**, Auth Posture, WAF rules |
| API9:2023 Improper Inventory Management | **API Discovery**, Schema learning, Auth Posture |
| API10:2023 Unsafe API Consumption | JWT validation, Schema validation, WAF managed |
## Monitoring
**Security Events:** `Security > Events` → Filter: Action = block, Service = API Shield
**Firewall Analytics:** `Analytics > Security` → Filter by `cf.api_gateway.*` fields
**Logpush fields:** APIGatewayAuthIDPresent, APIGatewayRequestViolatesSchema, APIGatewayFallthroughDetected, JWTValidationResult
## Availability (2026)
| Feature | Availability | Notes |
|---------|-------------|-------|
| mTLS (CF-managed CA) | All plans | Self-service |
| Endpoint Management | All plans | Limited operations |
| Schema Validation 2.0 | All plans | Limited operations |
| API Discovery | Enterprise | 10K+ ops |
| JWT Validation | Enterprise add-on | Full validation |
| BOLA Detection | Enterprise add-on | Requires session IDs |
| Auth Posture | Enterprise add-on | Security audit |
| Volumetric Abuse Detection | Enterprise add-on | Traffic analysis |
| GraphQL Protection | Enterprise add-on | Query limits |
| Sequence Mitigation | Enterprise (beta) | Contact team |
| Full Suite | Enterprise add-on | All features |
**Enterprise limits:** 10K operations (contact for higher). Preview access available for non-contract evaluation.
## See Also
- [configuration.md](configuration.md) - Setup all features before creating rules
- [api.md](api.md) - Firewall field reference and API endpoints
- [gotchas.md](gotchas.md) - Common issues and limits

View File

@@ -0,0 +1,65 @@
# Cloudflare API Integration
Guide for working with Cloudflare's REST API - authentication, SDK usage, common patterns, and troubleshooting.
## Quick Decision Tree
```
How are you calling the Cloudflare API?
├─ From Workers runtime → Use bindings, not REST API (see ../bindings/)
├─ Server-side (Node/Python/Go) → Official SDK (see api.md)
├─ CLI/scripts → Wrangler or curl (see configuration.md)
├─ Infrastructure-as-code → See ../pulumi/ or ../terraform/
└─ One-off requests → curl examples (see api.md)
```
## SDK Selection
| Language | Package | Best For | Default Retries |
|----------|---------|----------|-----------------|
| TypeScript | `cloudflare` | Node.js, Bun, Next.js, Workers | 2 |
| Python | `cloudflare` | FastAPI, Django, scripts | 2 |
| Go | `cloudflare-go/v4` | CLI tools, microservices | 10 |
All SDKs are Stainless-generated from OpenAPI spec (consistent APIs).
## Authentication Methods
| Method | Security | Use Case | Scope |
|--------|----------|----------|-------|
| **API Token** ✓ | Scoped, rotatable | Production | Per-zone or account |
| API Key + Email | Full account access | Legacy only | Everything |
| User Service Key | Limited | Origin CA certs only | Origin CA |
**Always use API tokens** for new projects.
## Rate Limits
| Limit | Value |
|-------|-------|
| Per user/token | 1200 requests / 5 minutes |
| Per IP | 200 requests / second |
| GraphQL | 320 / 5 minutes (cost-based) |
## Reading Order
| Task | Files to Read |
|------|---------------|
| Initialize SDK client | api.md |
| Configure auth/timeout/retry | configuration.md |
| Find usage patterns | patterns.md |
| Debug errors/rate limits | gotchas.md |
| Product-specific APIs | ../workers/, ../r2/, ../kv/, etc. |
## In This Reference
- **[api.md](api.md)** - SDK client initialization, pagination, error handling, examples
- **[configuration.md](configuration.md)** - Environment variables, SDK config, Wrangler setup
- **[patterns.md](patterns.md)** - Real-world patterns, batch operations, workflows
- **[gotchas.md](gotchas.md)** - Rate limits, SDK-specific issues, troubleshooting
## See Also
- [Cloudflare API Docs](https://developers.cloudflare.com/api/)
- [Bindings Reference](../bindings/) - Workers runtime bindings (preferred over REST API)
- [Wrangler Reference](../wrangler/) - CLI tool for Cloudflare development

View File

@@ -0,0 +1,204 @@
# API Reference
## Client Initialization
### TypeScript
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
});
```
### Python
```python
from cloudflare import Cloudflare
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
# For async:
from cloudflare import AsyncCloudflare
client = AsyncCloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
```
### Go
```go
import (
"github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/option"
)
client := cloudflare.NewClient(
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
)
```
## Authentication
### API Token (Recommended)
**Create token**: Dashboard → My Profile → API Tokens → Create Token
```bash
export CLOUDFLARE_API_TOKEN='your-token-here'
curl "https://api.cloudflare.com/client/v4/zones" \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
```
**Token scopes**: Always use minimal permissions (zone-specific, time-limited).
### API Key (Legacy)
```bash
curl "https://api.cloudflare.com/client/v4/zones" \
--header "X-Auth-Email: user@example.com" \
--header "X-Auth-Key: $CLOUDFLARE_API_KEY"
```
**Not recommended:** Full account access, cannot scope permissions.
## Auto-Pagination
All SDKs support automatic pagination for list operations.
```typescript
// TypeScript: for await...of
for await (const zone of client.zones.list()) {
console.log(zone.id);
}
```
```python
# Python: iterator protocol
for zone in client.zones.list():
print(zone.id)
```
```go
// Go: ListAutoPaging
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
for iter.Next() {
zone := iter.Current()
fmt.Println(zone.ID)
}
```
## Error Handling
```typescript
try {
const zone = await client.zones.get({ zone_id: 'xxx' });
} catch (err) {
if (err instanceof Cloudflare.NotFoundError) {
// 404
} else if (err instanceof Cloudflare.RateLimitError) {
// 429 - SDK auto-retries with backoff
} else if (err instanceof Cloudflare.APIError) {
console.log(err.status, err.message);
}
}
```
**Common Error Types:**
- `AuthenticationError` (401) - Invalid token
- `PermissionDeniedError` (403) - Insufficient scope
- `NotFoundError` (404) - Resource not found
- `RateLimitError` (429) - Rate limit exceeded
- `InternalServerError` (≥500) - Cloudflare error
## Zone Management
```typescript
// List zones
const zones = await client.zones.list({
account: { id: 'account-id' },
status: 'active',
});
// Create zone
const zone = await client.zones.create({
account: { id: 'account-id' },
name: 'example.com',
type: 'full', // or 'partial'
});
// Update zone
await client.zones.edit('zone-id', {
paused: false,
});
// Delete zone
await client.zones.delete('zone-id');
```
```go
// Go: requires cloudflare.F() wrapper
zone, err := client.Zones.New(ctx, cloudflare.ZoneNewParams{
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
ID: cloudflare.F("account-id"),
}),
Name: cloudflare.F("example.com"),
Type: cloudflare.F(cloudflare.ZoneNewParamsTypeFull),
})
```
## DNS Management
```typescript
// Create DNS record
await client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: 'subdomain.example.com',
content: '192.0.2.1',
ttl: 1, // auto
proxied: true, // Orange cloud
});
// List DNS records (with auto-pagination)
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
console.log(record.name, record.content);
}
// Update DNS record
await client.dns.records.update({
zone_id: 'zone-id',
dns_record_id: 'record-id',
type: 'A',
name: 'subdomain.example.com',
content: '203.0.113.1',
proxied: true,
});
// Delete DNS record
await client.dns.records.delete({
zone_id: 'zone-id',
dns_record_id: 'record-id',
});
```
```python
# Python example
client.dns.records.create(
zone_id="zone-id",
type="A",
name="subdomain.example.com",
content="192.0.2.1",
ttl=1,
proxied=True,
)
```
## See Also
- [configuration.md](./configuration.md) - SDK configuration, environment variables
- [patterns.md](./patterns.md) - Real-world patterns and workflows
- [gotchas.md](./gotchas.md) - Rate limits, troubleshooting

View File

@@ -0,0 +1,160 @@
# Configuration
## Environment Variables
### Set Variables
| Platform | Command |
|----------|---------|
| Linux/macOS | `export CLOUDFLARE_API_TOKEN='token'` |
| PowerShell | `$env:CLOUDFLARE_API_TOKEN = 'token'` |
| Windows CMD | `set CLOUDFLARE_API_TOKEN=token` |
**Security:** Never commit tokens. Use `.env` files (gitignored) or secret managers.
### .env File Pattern
```bash
# .env (add to .gitignore)
CLOUDFLARE_API_TOKEN=your-token-here
CLOUDFLARE_ACCOUNT_ID=your-account-id
```
```typescript
// TypeScript
import 'dotenv/config';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
});
```
```python
# Python
from dotenv import load_dotenv
load_dotenv()
client = Cloudflare(api_token=os.environ["CLOUDFLARE_API_TOKEN"])
```
## SDK Configuration
### TypeScript
```typescript
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
timeout: 120000, // 2 min (default 60s), in milliseconds
maxRetries: 5, // default 2
baseURL: 'https://...', // proxy (rare)
});
// Per-request overrides
await client.zones.get(
{ zone_id: 'zone-id' },
{ timeout: 5000, maxRetries: 0 }
);
```
### Python
```python
client = Cloudflare(
api_token=os.environ["CLOUDFLARE_API_TOKEN"],
timeout=120, # seconds (default 60)
max_retries=5, # default 2
base_url="https://...", # proxy (rare)
)
# Per-request overrides
client.with_options(timeout=5, max_retries=0).zones.get(zone_id="zone-id")
```
### Go
```go
client := cloudflare.NewClient(
option.WithAPIToken(os.Getenv("CLOUDFLARE_API_TOKEN")),
option.WithMaxRetries(5), // default 10 (higher than TS/Python)
option.WithRequestTimeout(2 * time.Minute), // default 60s
option.WithBaseURL("https://..."), // proxy (rare)
)
// Per-request overrides
client.Zones.Get(ctx, "zone-id", option.WithMaxRetries(0))
```
## Configuration Options
| Option | TypeScript | Python | Go | Default |
|--------|-----------|--------|-----|---------|
| Timeout | `timeout` (ms) | `timeout` (s) | `WithRequestTimeout` | 60s |
| Retries | `maxRetries` | `max_retries` | `WithMaxRetries` | 2 (Go: 10) |
| Base URL | `baseURL` | `base_url` | `WithBaseURL` | api.cloudflare.com |
**Note:** Go SDK has higher default retries (10) than TypeScript/Python (2).
## Timeout Configuration
**When to increase:**
- Large zone transfers
- Bulk DNS operations
- Worker script uploads
```typescript
const client = new Cloudflare({
timeout: 300000, // 5 minutes
});
```
## Retry Configuration
**When to increase:** Rate-limit-heavy workflows, flaky network
**When to decrease:** Fast-fail requirements, user-facing requests
```typescript
// Increase retries for batch operations
const client = new Cloudflare({ maxRetries: 10 });
// Disable retries for fast-fail
const fastClient = new Cloudflare({ maxRetries: 0 });
```
## Wrangler CLI Integration
```bash
# Configure authentication
wrangler login
# Or
export CLOUDFLARE_API_TOKEN='token'
# Common commands that use API
wrangler deploy # Uploads worker via API
wrangler kv:key put # KV operations
wrangler r2 bucket create # R2 operations
wrangler d1 execute # D1 operations
wrangler pages deploy # Pages operations
# Get API configuration
wrangler whoami # Shows authenticated user
```
### wrangler.toml
```toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
account_id = "your-account-id"
# Can also use env vars:
# CLOUDFLARE_ACCOUNT_ID
# CLOUDFLARE_API_TOKEN
```
## See Also
- [api.md](./api.md) - Client initialization, authentication
- [gotchas.md](./gotchas.md) - Rate limits, timeout errors
- [Wrangler Reference](../wrangler/) - CLI tool details

View File

@@ -0,0 +1,225 @@
# Gotchas & Troubleshooting
## Rate Limits & 429 Errors
**Actual Limits:**
- **1200 requests / 5 minutes** per user/token (global)
- **200 requests / second** per IP address
- **GraphQL: 320 / 5 minutes** (cost-based)
**SDK Behavior:**
- Auto-retry with exponential backoff (default 2 retries, Go: 10)
- Respects `Retry-After` header
- Throws `RateLimitError` after exhausting retries
**Solution:**
```typescript
// Increase retries for rate-limit-heavy workflows
const client = new Cloudflare({ maxRetries: 5 });
// Add application-level throttling
import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent requests
```
## SDK-Specific Issues
### Go: Required Field Wrapper
**Problem:** Go SDK requires `cloudflare.F()` wrapper for optional fields.
```go
// ❌ WRONG - Won't compile or send field
client.Zones.New(ctx, cloudflare.ZoneNewParams{
Name: "example.com",
})
// ✅ CORRECT
client.Zones.New(ctx, cloudflare.ZoneNewParams{
Name: cloudflare.F("example.com"),
Account: cloudflare.F(cloudflare.ZoneNewParamsAccount{
ID: cloudflare.F("account-id"),
}),
})
```
**Why:** Distinguishes between zero value, null, and omitted fields.
### Python: Async vs Sync Clients
**Problem:** Using sync client in async context or vice versa.
```python
# ❌ WRONG - Can't await sync client
from cloudflare import Cloudflare
client = Cloudflare()
await client.zones.list() # TypeError
# ✅ CORRECT - Use AsyncCloudflare
from cloudflare import AsyncCloudflare
client = AsyncCloudflare()
await client.zones.list()
```
## Token Permission Errors (403)
**Problem:** API returns 403 Forbidden despite valid token.
**Cause:** Token lacks required permissions (scope).
**Scopes Required:**
| Operation | Required Scope |
|-----------|----------------|
| List zones | Zone:Read (zone-level or account-level) |
| Create zone | Zone:Edit (account-level) |
| Edit DNS | DNS:Edit (zone-level) |
| Deploy Worker | Workers Script:Edit (account-level) |
| Read KV | Workers KV Storage:Read |
| Write KV | Workers KV Storage:Edit |
**Solution:** Re-create token with correct permissions in Dashboard → My Profile → API Tokens.
## Pagination Truncation
**Problem:** Only getting first 20 results (default page size).
**Solution:** Use auto-pagination iterators.
```typescript
// ❌ WRONG - Only first page (20 items)
const page = await client.zones.list();
// ✅ CORRECT - All results
const zones = [];
for await (const zone of client.zones.list()) {
zones.push(zone);
}
```
## Workers Subrequests
**Problem:** Rate limit hit faster than expected in Workers.
**Cause:** Workers subrequests count as separate API calls.
**Solution:** Use bindings instead of REST API in Workers (see ../bindings/).
```typescript
// ❌ WRONG - REST API in Workers (counts against rate limit)
const client = new Cloudflare({ apiToken: env.CLOUDFLARE_API_TOKEN });
const zones = await client.zones.list();
// ✅ CORRECT - Use bindings (no rate limit)
// Access via env.MY_BINDING
```
## Authentication Errors (401)
**Problem:** "Authentication failed" or "Invalid token"
**Causes:**
- Token expired
- Token deleted/revoked
- Token not set in environment
- Wrong token format
**Solution:**
```typescript
// Verify token is set
if (!process.env.CLOUDFLARE_API_TOKEN) {
throw new Error('CLOUDFLARE_API_TOKEN not set');
}
// Test token
const user = await client.user.tokens.verify();
console.log('Token valid:', user.status);
```
## Timeout Errors
**Problem:** Request times out (default 60s).
**Cause:** Large operations (bulk DNS, zone transfers).
**Solution:** Increase timeout or split operations.
```typescript
// Increase timeout
const client = new Cloudflare({
timeout: 300000, // 5 minutes
});
// Or split operations
const batchSize = 100;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
await processBatch(batch);
}
```
## Zone Not Found (404)
**Problem:** Zone ID valid but returns 404.
**Causes:**
- Zone not in account associated with token
- Zone deleted
- Wrong zone ID format
**Solution:**
```typescript
// List all zones to find correct ID
for await (const zone of client.zones.list()) {
console.log(zone.id, zone.name);
}
```
## Limits Reference
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| API rate limit | 1200/5min | Per user/token |
| IP rate limit | 200/sec | Per IP |
| GraphQL rate limit | 320/5min | Cost-based |
| Parallel requests (recommended) | < 10 | Avoid overwhelming API |
| Default page size | 20 | Use auto-pagination |
| Max page size | 50 | Some endpoints |
## Best Practices
**Security:**
- Never commit tokens
- Use minimal permissions
- Rotate tokens regularly
- Set token expiration
**Performance:**
- Batch operations
- Use pagination wisely
- Cache responses
- Handle rate limits
**Code Organization:**
```typescript
// Create reusable client instance
export const cfClient = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
maxRetries: 5,
});
// Wrap common operations
export async function getZoneDetails(zoneId: string) {
return await cfClient.zones.get({ zone_id: zoneId });
}
```
## See Also
- [api.md](./api.md) - Error types, authentication
- [configuration.md](./configuration.md) - Timeout/retry configuration
- [patterns.md](./patterns.md) - Error handling patterns

View File

@@ -0,0 +1,204 @@
# Common Patterns
## List All with Auto-Pagination
**Problem:** API returns paginated results. Default page size is 20.
**Solution:** Use SDK auto-pagination to iterate all results.
```typescript
// TypeScript
for await (const zone of client.zones.list()) {
console.log(zone.name);
}
```
```python
# Python
for zone in client.zones.list():
print(zone.name)
```
```go
// Go
iter := client.Zones.ListAutoPaging(ctx, cloudflare.ZoneListParams{})
for iter.Next() {
fmt.Println(iter.Current().Name)
}
```
## Error Handling with Retry
**Problem:** Rate limits (429) and transient errors need retry.
**Solution:** SDKs auto-retry with exponential backoff. Customize as needed.
```typescript
// Increase retries for rate-limit-heavy operations
const client = new Cloudflare({ maxRetries: 5 });
try {
const zone = await client.zones.create({ /* ... */ });
} catch (err) {
if (err instanceof Cloudflare.RateLimitError) {
// Already retried 5 times with backoff
const retryAfter = err.headers['retry-after'];
console.log(`Rate limited. Retry after ${retryAfter}s`);
}
}
```
## Batch Parallel Operations
**Problem:** Need to create multiple resources quickly.
**Solution:** Use `Promise.all()` for parallel requests (respect rate limits).
```typescript
// Create multiple DNS records in parallel
const records = ['www', 'api', 'cdn'].map(subdomain =>
client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: `${subdomain}.example.com`,
content: '192.0.2.1',
})
);
await Promise.all(records);
```
**Controlled concurrency** (avoid rate limits):
```typescript
import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent
const subdomains = ['www', 'api', 'cdn', /* many more */];
const records = subdomains.map(subdomain =>
limit(() => client.dns.records.create({
zone_id: 'zone-id',
type: 'A',
name: `${subdomain}.example.com`,
content: '192.0.2.1',
}))
);
await Promise.all(records);
```
## Zone CRUD Workflow
```typescript
// Create
const zone = await client.zones.create({
account: { id: 'account-id' },
name: 'example.com',
type: 'full',
});
// Read
const fetched = await client.zones.get({ zone_id: zone.id });
// Update
await client.zones.edit(zone.id, { paused: false });
// Delete
await client.zones.delete(zone.id);
```
## DNS Bulk Update
```typescript
// Fetch all A records
const records = [];
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
records.push(record);
}
// Update all to new IP
await Promise.all(records.map(record =>
client.dns.records.update({
zone_id: 'zone-id',
dns_record_id: record.id,
type: 'A',
name: record.name,
content: '203.0.113.1', // New IP
proxied: record.proxied,
ttl: record.ttl,
})
));
```
## Filter and Collect Results
```typescript
// Find all proxied A records
const proxiedRecords = [];
for await (const record of client.dns.records.list({
zone_id: 'zone-id',
type: 'A',
})) {
if (record.proxied) {
proxiedRecords.push(record);
}
}
```
## Error Recovery Pattern
```typescript
async function createZoneWithRetry(name: string, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await client.zones.create({
account: { id: 'account-id' },
name,
type: 'full',
});
} catch (err) {
if (err instanceof Cloudflare.RateLimitError && attempt < maxAttempts) {
const retryAfter = parseInt(err.headers['retry-after'] || '5');
console.log(`Rate limited, waiting ${retryAfter}s (retry ${attempt}/${maxAttempts})`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
} else {
throw err;
}
}
}
}
```
## Conditional Update Pattern
```typescript
// Only update if zone is active
const zone = await client.zones.get({ zone_id: 'zone-id' });
if (zone.status === 'active') {
await client.zones.edit(zone.id, { paused: false });
}
```
## Batch with Error Handling
```typescript
// Process multiple zones, continue on errors
const results = await Promise.allSettled(
zoneIds.map(id => client.zones.get({ zone_id: id }))
);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`Zone ${i}: ${result.value.name}`);
} else {
console.error(`Zone ${i} failed:`, result.reason.message);
}
});
```
## See Also
- [api.md](./api.md) - SDK client initialization, basic operations
- [gotchas.md](./gotchas.md) - Rate limits, common errors
- [configuration.md](./configuration.md) - SDK configuration options

View File

@@ -0,0 +1,90 @@
# Cloudflare Argo Smart Routing Skill Reference
## Overview
Cloudflare Argo Smart Routing is a performance optimization service that detects real-time network issues and routes web traffic across the most efficient network path. It continuously monitors network conditions and intelligently routes traffic through the fastest, most reliable routes in Cloudflare's network.
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product for enhanced DDoS protection and performance. Existing Argo customers maintain full functionality with gradual migration to Smart Shield features.
## Quick Start
### Enable via cURL
```bash
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "on"}'
```
### Enable via TypeScript SDK
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
const result = await client.argo.smartRouting.edit({
zone_id: 'your-zone-id',
value: 'on',
});
console.log(`Argo enabled: ${result.value}`);
```
## Core Concepts
### What It Does
- **Intelligent routing**: Detects congestion, outages, packet loss in real-time
- **Global optimization**: Routes across 300+ Cloudflare data centers
- **Automatic failover**: Switches paths when issues detected (typically <1s)
- **Works with existing setup**: No origin changes required
### Billing Model
- Usage-based: Charged per GB of traffic (excluding DDoS/WAF mitigated traffic)
- Requires billing configuration before enabling
- Available on Enterprise+ plans (check zone eligibility)
### When to Use
- **High-traffic production sites** with global user base
- **Latency-sensitive applications** (APIs, real-time services)
- **Sites behind Cloudflare proxy** (orange-clouded DNS records)
- **Combined with Tiered Cache** for maximum performance gains
### When NOT to Use
- Development/staging environments (cost control)
- Low-traffic sites (<1TB/month) where cost may exceed benefit
- Sites with primarily single-region traffic
## Should I Enable Argo?
| Your Situation | Recommendation |
|----------------|----------------|
| Global production app, >1TB/month traffic | ✅ Enable - likely ROI positive |
| Enterprise plan, latency-critical APIs | ✅ Enable - performance matters |
| Regional site, <100GB/month traffic | ⚠️ Evaluate - cost may not justify |
| Development/staging environment | ❌ Disable - use in production only |
| Not yet configured billing | ❌ Configure billing first |
## Reading Order by Task
| Your Goal | Start With | Then Read |
|-----------|------------|-----------|
| Enable Argo for first time | Quick Start above → [configuration.md](configuration.md) | [gotchas.md](gotchas.md) |
| Use TypeScript/Python SDK | [api.md](api.md) | [patterns.md](patterns.md) |
| Terraform/IaC setup | [configuration.md](configuration.md) | - |
| Enable for Spectrum TCP app | [patterns.md](patterns.md) → Spectrum section | [api.md](api.md) |
| Troubleshoot enablement issue | [gotchas.md](gotchas.md) | [api.md](api.md) |
| Manage billing/usage | [patterns.md](patterns.md) → Billing section | [gotchas.md](gotchas.md) |
## In This Reference
- **[api.md](api.md)** - API endpoints, SDK methods, error handling, Python/TypeScript examples
- **[configuration.md](configuration.md)** - Terraform setup, environment config, billing configuration
- **[patterns.md](patterns.md)** - Tiered Cache integration, Spectrum TCP apps, billing management, validation patterns
- **[gotchas.md](gotchas.md)** - Common errors, permission issues, limits, best practices
## See Also
- [Cloudflare Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
- [Spectrum Documentation](https://developers.cloudflare.com/spectrum/)
- [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/)

View File

@@ -0,0 +1,240 @@
## API Reference
**Note on Smart Shield:** Argo Smart Routing is being integrated into Cloudflare's Smart Shield product. API endpoints remain stable; existing integrations continue to work without changes.
### Base Endpoint
```
https://api.cloudflare.com/client/v4
```
### Authentication
Use API tokens with Zone:Argo Smart Routing:Edit permissions:
```bash
# Headers required
X-Auth-Email: user@example.com
Authorization: Bearer YOUR_API_TOKEN
```
### Get Argo Smart Routing Status
**Endpoint:** `GET /zones/{zone_id}/argo/smart_routing`
**Description:** Retrieves current Argo Smart Routing enablement status.
**cURL Example:**
```bash
curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
```
**Response:**
```json
{
"result": {
"id": "smart_routing",
"value": "on",
"editable": true,
"modified_on": "2024-01-11T12:00:00Z"
},
"success": true,
"errors": [],
"messages": []
}
```
**TypeScript SDK Example:**
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN
});
const status = await client.argo.smartRouting.get({ zone_id: 'your-zone-id' });
console.log(`Argo status: ${status.value}, editable: ${status.editable}`);
```
**Python SDK Example:**
```python
from cloudflare import Cloudflare
client = Cloudflare(api_token=os.environ.get('CLOUDFLARE_API_TOKEN'))
status = client.argo.smart_routing.get(zone_id='your-zone-id')
print(f"Argo status: {status.value}, editable: {status.editable}")
```
### Update Argo Smart Routing Status
**Endpoint:** `PATCH /zones/{zone_id}/argo/smart_routing`
**Description:** Enable or disable Argo Smart Routing for a zone.
**Request Body:**
```json
{
"value": "on" // or "off"
}
```
**cURL Example:**
```bash
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{zone_id}/argo/smart_routing" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "on"}'
```
**TypeScript SDK Example:**
```typescript
const result = await client.argo.smartRouting.edit({
zone_id: 'your-zone-id',
value: 'on',
});
console.log(`Updated: ${result.value} at ${result.modified_on}`);
```
**Python SDK Example:**
```python
result = client.argo.smart_routing.edit(
zone_id='your-zone-id',
value='on'
)
print(f"Updated: {result.value} at {result.modified_on}")
```
## Checking Editability Before Updates
**Critical:** Always check the `editable` field before attempting to enable/disable Argo. When `editable: false`, the zone has restrictions (billing not configured, insufficient permissions, or plan limitations).
**Pattern:**
```typescript
async function safelyEnableArgo(client: Cloudflare, zoneId: string): Promise<boolean> {
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
if (!status.editable) {
console.error('Cannot modify Argo: editable=false (check billing/permissions)');
return false;
}
if (status.value === 'on') {
console.log('Argo already enabled');
return true;
}
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
console.log('Argo enabled successfully');
return true;
}
```
**Python Pattern:**
```python
def safely_enable_argo(client: Cloudflare, zone_id: str) -> bool:
status = client.argo.smart_routing.get(zone_id=zone_id)
if not status.editable:
print('Cannot modify Argo: editable=false (check billing/permissions)')
return False
if status.value == 'on':
print('Argo already enabled')
return True
client.argo.smart_routing.edit(zone_id=zone_id, value='on')
print('Argo enabled successfully')
return True
```
## Error Handling
The TypeScript SDK provides typed error classes for robust error handling:
```typescript
import Cloudflare from 'cloudflare';
import { APIError, APIConnectionError, RateLimitError } from 'cloudflare';
async function enableArgoWithErrorHandling(client: Cloudflare, zoneId: string) {
try {
const result = await client.argo.smartRouting.edit({
zone_id: zoneId,
value: 'on',
});
return result;
} catch (error) {
if (error instanceof RateLimitError) {
console.error('Rate limited. Retry after:', error.response?.headers.get('retry-after'));
// Implement exponential backoff
} else if (error instanceof APIError) {
console.error('API error:', error.status, error.message);
if (error.status === 403) {
console.error('Permission denied - check API token scopes');
} else if (error.status === 400) {
console.error('Bad request - verify zone_id and payload');
}
} else if (error instanceof APIConnectionError) {
console.error('Connection failed:', error.message);
// Retry with exponential backoff
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
```
**Python Error Handling:**
```python
from cloudflare import Cloudflare, APIError, RateLimitError
def enable_argo_with_error_handling(client: Cloudflare, zone_id: str):
try:
result = client.argo.smart_routing.edit(zone_id=zone_id, value='on')
return result
except RateLimitError as e:
print(f"Rate limited. Retry after: {e.response.headers.get('retry-after')}")
raise
except APIError as e:
print(f"API error: {e.status} - {e.message}")
if e.status == 403:
print('Permission denied - check API token scopes')
elif e.status == 400:
print('Bad request - verify zone_id and payload')
raise
except Exception as e:
print(f"Unexpected error: {e}")
raise
```
## Response Schema
All Argo Smart Routing API responses follow this structure:
```typescript
interface ArgoSmartRoutingResponse {
result: {
id: 'smart_routing';
value: 'on' | 'off';
editable: boolean;
modified_on: string; // ISO 8601 timestamp
};
success: boolean;
errors: Array<{
code: number;
message: string;
}>;
messages: Array<string>;
}
```
## Key Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `value` | `"on" \| "off"` | Current enablement status |
| `editable` | `boolean` | Whether changes are allowed (check before PATCH) |
| `modified_on` | `string` | ISO timestamp of last modification |
| `success` | `boolean` | Whether request succeeded |
| `errors` | `Array` | Error details if `success: false`

View File

@@ -0,0 +1,197 @@
## Configuration Management
**Note on Smart Shield Evolution:** Argo Smart Routing is being integrated into Smart Shield. Configuration methods below remain valid; Terraform and IaC patterns unchanged.
### Infrastructure as Code (Terraform)
```hcl
# terraform/argo.tf
# Note: Use Cloudflare Terraform provider
resource "cloudflare_argo" "example" {
zone_id = var.zone_id
smart_routing = "on"
tiered_caching = "on"
}
variable "zone_id" {
description = "Cloudflare Zone ID"
type = string
}
output "argo_enabled" {
value = cloudflare_argo.example.smart_routing
description = "Argo Smart Routing status"
}
```
### Environment-Based Configuration
```typescript
// config/argo.ts
interface ArgoEnvironmentConfig {
enabled: boolean;
tieredCache: boolean;
monitoring: {
usageAlerts: boolean;
threshold: number;
};
}
const configs: Record<string, ArgoEnvironmentConfig> = {
production: {
enabled: true,
tieredCache: true,
monitoring: {
usageAlerts: true,
threshold: 1000, // GB
},
},
staging: {
enabled: true,
tieredCache: false,
monitoring: {
usageAlerts: false,
threshold: 100, // GB
},
},
development: {
enabled: false,
tieredCache: false,
monitoring: {
usageAlerts: false,
threshold: 0,
},
},
};
export function getArgoConfig(env: string): ArgoEnvironmentConfig {
return configs[env] || configs.development;
}
```
### Pulumi Configuration
```typescript
// pulumi/argo.ts
import * as cloudflare from '@pulumi/cloudflare';
const zone = new cloudflare.Zone('example-zone', {
zone: 'example.com',
plan: 'enterprise',
});
const argoSettings = new cloudflare.Argo('argo-config', {
zoneId: zone.id,
smartRouting: 'on',
tieredCaching: 'on',
});
export const argoEnabled = argoSettings.smartRouting;
export const zoneId = zone.id;
```
## Billing Configuration
Before enabling Argo Smart Routing, ensure billing is configured for the account:
**Prerequisites:**
1. Valid payment method on file
2. Enterprise or higher plan
3. Zone must have billing enabled
**Check Billing Status via Dashboard:**
1. Navigate to Account → Billing
2. Verify payment method configured
3. Check zone subscription status
**Note:** Attempting to enable Argo without billing configured will result in `editable: false` in API responses.
## Environment Variable Setup
**Required Environment Variables:**
```bash
# .env
CLOUDFLARE_API_TOKEN=your_api_token_here
CLOUDFLARE_ZONE_ID=your_zone_id_here
CLOUDFLARE_ACCOUNT_ID=your_account_id_here
# Optional
ARGO_ENABLED=true
ARGO_TIERED_CACHE=true
```
**TypeScript Configuration Loader:**
```typescript
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
CLOUDFLARE_API_TOKEN: z.string().min(1),
CLOUDFLARE_ZONE_ID: z.string().min(1),
CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
ARGO_ENABLED: z.string().optional().default('false'),
ARGO_TIERED_CACHE: z.string().optional().default('false'),
});
export const env = envSchema.parse(process.env);
export const argoConfig = {
enabled: env.ARGO_ENABLED === 'true',
tieredCache: env.ARGO_TIERED_CACHE === 'true',
};
```
## CI/CD Integration
**GitHub Actions Example:**
```yaml
# .github/workflows/deploy-argo.yml
name: Deploy Argo Configuration
on:
push:
branches: [main]
paths:
- 'terraform/argo.tf'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Init
run: terraform init
working-directory: ./terraform
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: ./terraform
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TF_VAR_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
```
## Enterprise Preview Program
For early access to Argo Smart Routing features and Smart Shield integration:
**Eligibility:**
- Enterprise plan customers
- Active Cloudflare support contract
- Production traffic >100GB/month
**How to Join:**
1. Contact Cloudflare account team or support
2. Request Argo/Smart Shield preview access
3. Receive preview zone configuration
**Preview Features:**
- Enhanced analytics and reporting
- Smart Shield DDoS integration
- Advanced routing policies
- Priority support for routing issues

View File

@@ -0,0 +1,111 @@
## Best Practices Summary
**Smart Shield Note:** Argo Smart Routing evolving into Smart Shield. Best practices below remain applicable; monitor Cloudflare changelog for Smart Shield updates.
1. **Always check editability** before attempting to enable/disable Argo
2. **Set up billing notifications** to avoid unexpected costs
3. **Combine with Tiered Cache** for maximum performance benefit
4. **Use in production only** - disable for dev/staging to control costs
5. **Monitor analytics** - require 500+ requests in 48h for detailed metrics
6. **Handle errors gracefully** - check for billing, permissions, zone compatibility
7. **Test configuration changes** in staging before production
8. **Use TypeScript SDK** for type safety and better developer experience
9. **Implement retry logic** for API calls in production systems
10. **Document zone-specific settings** for team visibility
## Common Errors
### "Argo unavailable"
**Problem:** API returns error "Argo Smart Routing is unavailable for this zone"
**Cause:** Zone not eligible or billing not set up
**Solution:**
1. Verify zone has Enterprise or higher plan
2. Check billing is configured in Account → Billing
3. Ensure payment method is valid and current
4. Contact Cloudflare support if eligibility unclear
### "Cannot enable/disable"
**Problem:** API call succeeds but status remains unchanged, or `editable: false` in GET response
**Cause:** Insufficient permissions or zone restrictions
**Solution:**
1. Check API token has `Zone:Argo Smart Routing:Edit` permission
2. Verify `editable: true` in GET response before attempting PATCH
3. If `editable: false`, check:
- Billing configured for account
- Zone plan includes Argo (Enterprise+)
- No active zone holds or suspensions
- API token has correct scopes
### `editable: false` Error
**Problem:** GET request returns `"editable": false`, preventing enable/disable
**Cause:** Zone-level restrictions from billing, plan, or permissions
**Solution Pattern:**
```typescript
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
if (!status.editable) {
// Don't attempt to modify - will fail
console.error('Cannot modify Argo settings:');
console.error('- Check billing is configured');
console.error('- Verify zone has Enterprise+ plan');
console.error('- Confirm API token has Edit permission');
throw new Error('Argo is not editable for this zone');
}
// Safe to proceed with enable/disable
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
```
### Rate Limiting
**Problem:** `429 Too Many Requests` error from API
**Cause:** Exceeded API rate limits (typically 1200 requests per 5 minutes)
**Solution:**
```typescript
import { RateLimitError } from 'cloudflare';
try {
await client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' });
} catch (error) {
if (error instanceof RateLimitError) {
const retryAfter = error.response?.headers.get('retry-after');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, (retryAfter || 60) * 1000));
// Retry request
}
}
```
## Limits
| Resource/Limit | Value | Notes |
|----------------|-------|-------|
| Min requests for analytics | 500 in 48h | For detailed metrics via GraphQL |
| Zones supported | Enterprise+ | Check zone plan in dashboard |
| Billing requirement | Must be configured | Before enabling; verify payment method |
| API rate limit | 1200 req / 5 min | Per API token across all endpoints |
| Spectrum apps | No hard limit | Each app can enable Argo independently |
| Traffic counting | Proxied only | Only orange-clouded DNS records count |
| DDoS/WAF exemption | Yes | Mitigated traffic excluded from billing |
| Analytics latency | 1-5 minutes | Real-time metrics not available |
## Additional Resources
- [Official Argo Smart Routing Docs](https://developers.cloudflare.com/argo-smart-routing/)
- [Cloudflare Smart Shield](https://developers.cloudflare.com/smart-shield/)
- [API Authentication](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/)
- [Cloudflare TypeScript SDK](https://github.com/cloudflare/cloudflare-typescript)
- [Cloudflare Python SDK](https://github.com/cloudflare/cloudflare-python)

View File

@@ -0,0 +1,104 @@
# Integration Patterns
## Enable Argo + Tiered Cache
```typescript
async function enableOptimalPerformance(client: Cloudflare, zoneId: string) {
await Promise.all([
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
]);
}
```
**Flow:** Visitor → Edge (Lower-Tier) → [Cache Miss] → Upper-Tier → [Cache Miss + Argo] → Origin
**Impact:** Argo ~30% latency reduction + Tiered Cache 50-80% origin offload
## Usage Analytics (GraphQL)
```graphql
query ArgoAnalytics($zoneTag: string!) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequestsAdaptiveGroups(limit: 1000) {
sum { argoBytes, bytes }
}
}
}
}
```
**Billing:** ~$0.10/GB. DDoS-mitigated and WAF-blocked traffic NOT charged.
## Spectrum TCP Integration
Enable Argo for non-HTTP traffic (databases, game servers, IoT):
```typescript
// Update existing app
await client.spectrum.apps.update(appId, { zone_id: zoneId, argo_smart_routing: true });
// Create new app with Argo
await client.spectrum.apps.create({
zone_id: zoneId,
dns: { type: 'CNAME', name: 'tcp.example.com' },
origin_direct: ['tcp://origin.example.com:3306'],
protocol: 'tcp/3306',
argo_smart_routing: true,
});
```
**Use cases:** MySQL/PostgreSQL (3306/5432), game servers, MQTT (1883), SSH (22)
## Pre-Flight Validation
```typescript
async function validateArgoEligibility(client: Cloudflare, zoneId: string) {
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
const zone = await client.zones.get({ zone_id: zoneId });
const issues: string[] = [];
if (!status.editable) issues.push('Zone not editable');
if (['free', 'pro'].includes(zone.plan.legacy_id)) issues.push('Requires Business+ plan');
if (zone.status !== 'active') issues.push('Zone not active');
return { canEnable: issues.length === 0, issues };
}
```
## Post-Enable Verification
```typescript
async function verifyArgoEnabled(client: Cloudflare, zoneId: string): Promise<boolean> {
await new Promise(r => setTimeout(r, 2000)); // Wait for propagation
const status = await client.argo.smartRouting.get({ zone_id: zoneId });
return status.value === 'on';
}
```
## Full Setup Pattern
```typescript
async function setupArgo(client: Cloudflare, zoneId: string) {
// 1. Validate
const { canEnable, issues } = await validateArgoEligibility(client, zoneId);
if (!canEnable) throw new Error(issues.join(', '));
// 2. Enable both features
await Promise.all([
client.argo.smartRouting.edit({ zone_id: zoneId, value: 'on' }),
client.argo.tieredCaching.edit({ zone_id: zoneId, value: 'on' }),
]);
// 3. Verify
const [argo, cache] = await Promise.all([
client.argo.smartRouting.get({ zone_id: zoneId }),
client.argo.tieredCaching.get({ zone_id: zoneId }),
]);
return { argo: argo.value === 'on', tieredCache: cache.value === 'on' };
}
```
**When to combine:** High-traffic sites (>1TB/mo), global users, cacheable content.

View File

@@ -0,0 +1,122 @@
# Cloudflare Bindings Skill Reference
Expert guidance on Cloudflare Workers Bindings - the runtime APIs that connect Workers to Cloudflare platform resources.
## What Are Bindings?
Bindings are how Workers access Cloudflare resources (storage, compute, services) via the `env` object. They're configured in `wrangler.jsonc`, type-safe via TypeScript, and zero-overhead at runtime.
## Reading Order
1. **This file** - Binding catalog and selection guide
2. **[api.md](api.md)** - TypeScript types and env access patterns
3. **[configuration.md](configuration.md)** - Complete wrangler.jsonc examples
4. **[patterns.md](patterns.md)** - Best practices and common patterns
5. **[gotchas.md](gotchas.md)** - Critical pitfalls and troubleshooting
## Binding Catalog
### Storage Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **KV** | Key-value cache, CDN-backed reads | `env.MY_KV.get(key)` |
| **R2** | Object storage (S3-compatible) | `env.MY_BUCKET.get(key)` |
| **D1** | SQL database (SQLite) | `env.DB.prepare(sql).all()` |
| **Durable Objects** | Coordination, real-time state | `env.MY_DO.get(id)` |
| **Vectorize** | Vector embeddings search | `env.VECTORIZE.query(vector)` |
| **Queues** | Async message processing | `env.MY_QUEUE.send(msg)` |
### Compute Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Service** | Worker-to-Worker RPC | `env.MY_SERVICE.fetch(req)` |
| **Workers AI** | LLM inference | `env.AI.run(model, input)` |
| **Browser Rendering** | Headless Chrome | `env.BROWSER.fetch(url)` |
### Platform Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Analytics Engine** | Custom metrics | `env.ANALYTICS.writeDataPoint(data)` |
| **mTLS** | Client certificates | `env.MY_CERT` (string) |
| **Hyperdrive** | Database pooling | `env.HYPERDRIVE.connectionString` |
| **Rate Limiting** | Request throttling | `env.RATE_LIMITER.limit(id)` |
| **Workflows** | Long-running workflows | `env.MY_WORKFLOW.create()` |
### Configuration Bindings
| Binding | Use Case | Access Pattern |
|---------|----------|----------------|
| **Environment Variables** | Non-sensitive config | `env.API_URL` (string) |
| **Secrets** | Sensitive values | `env.API_KEY` (string) |
| **Text/Data Blobs** | Static files | `env.MY_BLOB` (string) |
| **WASM** | WebAssembly modules | `env.MY_WASM` (WebAssembly.Module) |
## Quick Selection Guide
**Need persistent storage?**
- Key-value < 25MB → **KV**
- Files/objects → **R2**
- Relational data → **D1**
- Real-time coordination → **Durable Objects**
**Need AI/compute?**
- LLM inference → **Workers AI**
- Scraping/PDFs → **Browser Rendering**
- Call another Worker → **Service binding**
**Need async processing?**
- Background jobs → **Queues**
**Need config?**
- Public values → **Environment Variables**
- Secrets → **Secrets** (never commit)
## Quick Start
1. **Add binding to wrangler.jsonc:**
```jsonc
{
"kv_namespaces": [
{ "binding": "MY_KV", "id": "your-kv-id" }
]
}
```
2. **Generate types:**
```bash
npx wrangler types
```
3. **Access in Worker:**
```typescript
export default {
async fetch(request, env, ctx) {
await env.MY_KV.put('key', 'value');
return new Response('OK');
}
}
```
## Type Safety
Bindings are fully typed via `wrangler types`. See [api.md](api.md) for details.
## Limits
- 64 bindings max per Worker (all types combined)
- See [gotchas.md](gotchas.md) for per-binding limits
## Key Concepts
**Zero-overhead access:** Bindings compiled into Worker, no network calls to access
**Type-safe:** Full TypeScript support via `wrangler types`
**Per-environment:** Different IDs for dev/staging/production
**Secrets vs Vars:** Secrets encrypted at rest, never in config files
## See Also
- [Cloudflare Docs: Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)

View File

@@ -0,0 +1,203 @@
# Bindings API Reference
## TypeScript Types
Cloudflare generates binding types via `npx wrangler types`. This creates `.wrangler/types/runtime.d.ts` with your Env interface.
### Generated Env Interface
After running `wrangler types`, TypeScript knows your bindings:
```typescript
interface Env {
// From wrangler.jsonc bindings
MY_KV: KVNamespace;
MY_BUCKET: R2Bucket;
DB: D1Database;
MY_SERVICE: Fetcher;
AI: Ai;
// From vars
API_URL: string;
// From secrets (set via wrangler secret put)
API_KEY: string;
}
```
### Binding Types
| Config | TypeScript Type | Package |
|--------|-----------------|---------|
| `kv_namespaces` | `KVNamespace` | `@cloudflare/workers-types` |
| `r2_buckets` | `R2Bucket` | `@cloudflare/workers-types` |
| `d1_databases` | `D1Database` | `@cloudflare/workers-types` |
| `durable_objects.bindings` | `DurableObjectNamespace` | `@cloudflare/workers-types` |
| `vectorize` | `VectorizeIndex` | `@cloudflare/workers-types` |
| `queues.producers` | `Queue` | `@cloudflare/workers-types` |
| `services` | `Fetcher` | `@cloudflare/workers-types` |
| `ai` | `Ai` | `@cloudflare/workers-types` |
| `browser` | `Fetcher` | `@cloudflare/workers-types` |
| `analytics_engine_datasets` | `AnalyticsEngineDataset` | `@cloudflare/workers-types` |
| `hyperdrive` | `Hyperdrive` | `@cloudflare/workers-types` |
| `rate_limiting` | `RateLimit` | `@cloudflare/workers-types` |
| `workflows` | `Workflow` | `@cloudflare/workers-types` |
| `mtls_certificates` / `vars` / `text_blobs` / `data_blobs` | `string` | Built-in |
| `wasm_modules` | `WebAssembly.Module` | Built-in |
## Accessing Bindings
### Method 1: fetch() Handler (Recommended)
```typescript
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const value = await env.MY_KV.get('key');
return new Response(value);
}
}
```
**Why:** Type-safe, aligns with Workers API, supports ctx for waitUntil/passThroughOnException.
### Method 2: Hono Framework
```typescript
import { Hono } from 'hono';
const app = new Hono<{ Bindings: Env }>();
app.get('/', async (c) => {
const value = await c.env.MY_KV.get('key');
return c.json({ value });
});
export default app;
```
**Why:** c.env auto-typed, ergonomic for routing-heavy apps.
### Method 3: Module Workers (Legacy)
```typescript
export async function handleRequest(request: Request, env: Env): Promise<Response> {
const value = await env.MY_KV.get('key');
return new Response(value);
}
addEventListener('fetch', (event) => {
// env not directly available - requires workarounds
});
```
**Avoid:** Use fetch() handler instead (Method 1).
## Type Generation Workflow
### Initial Setup
```bash
# Install wrangler
npm install -D wrangler
# Generate types from wrangler.jsonc
npx wrangler types
```
### After Changing Bindings
```bash
# Added/modified binding in wrangler.jsonc
npx wrangler types
# TypeScript now sees updated Env interface
```
**Note:** `wrangler types` outputs to `.wrangler/types/runtime.d.ts`. TypeScript picks this up automatically if `@cloudflare/workers-types` is in `tsconfig.json` `"types"` array.
## Key Binding Methods
**KV:**
```typescript
await env.MY_KV.get(key, { type: 'json' }); // text|json|arrayBuffer|stream
await env.MY_KV.put(key, value, { expirationTtl: 3600 });
await env.MY_KV.delete(key);
await env.MY_KV.list({ prefix: 'user:' });
```
**R2:**
```typescript
await env.BUCKET.get(key);
await env.BUCKET.put(key, value);
await env.BUCKET.delete(key);
await env.BUCKET.list({ prefix: 'images/' });
```
**D1:**
```typescript
await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
await env.DB.batch([stmt1, stmt2]);
```
**Service:**
```typescript
await env.MY_SERVICE.fetch(new Request('https://fake/path'));
```
**Workers AI:**
```typescript
await env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });
```
**Queues:**
```typescript
await env.MY_QUEUE.send({ userId: 123, action: 'process' });
```
**Durable Objects:**
```typescript
const id = env.MY_DO.idFromName('user-123');
const stub = env.MY_DO.get(id);
await stub.fetch(new Request('https://fake/increment'));
```
## Runtime vs Build-Time Types
| Type Source | When Generated | Use Case |
|-------------|----------------|----------|
| `@cloudflare/workers-types` | npm install | Base Workers APIs (Request, Response, etc.) |
| `wrangler types` | After config change | Your specific bindings (Env interface) |
**Install both:**
```bash
npm install -D @cloudflare/workers-types
npx wrangler types
```
## Type Safety Best Practices
1. **Never use `any` for env:**
```typescript
// ❌ BAD
async fetch(request: Request, env: any) { }
// ✅ GOOD
async fetch(request: Request, env: Env) { }
```
2. **Run wrangler types after config changes:**
```bash
# After editing wrangler.jsonc
npx wrangler types
```
3. **Check generated types match config:**
```bash
# View generated Env interface
cat .wrangler/types/runtime.d.ts
```
## See Also
- [Workers Types Package](https://www.npmjs.com/package/@cloudflare/workers-types)
- [Wrangler Types Command](https://developers.cloudflare.com/workers/wrangler/commands/#types)

View File

@@ -0,0 +1,188 @@
# Binding Configuration Reference
## Storage Bindings
```jsonc
{
"kv_namespaces": [{ "binding": "MY_KV", "id": "..." }],
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "..." }],
"durable_objects": { "bindings": [{ "name": "MY_DO", "class_name": "MyDO" }] },
"vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],
"queues": { "producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }] }
}
```
**Create commands:**
```bash
npx wrangler kv namespace create MY_KV
npx wrangler r2 bucket create my-bucket
npx wrangler d1 create my-db
npx wrangler vectorize create my-index --dimensions=768 --metric=cosine
npx wrangler queues create my-queue
# List existing resources
npx wrangler kv namespace list
npx wrangler r2 bucket list
npx wrangler d1 list
npx wrangler vectorize list
npx wrangler queues list
```
## Compute Bindings
```jsonc
{
"services": [{
"binding": "MY_SERVICE",
"service": "other-worker",
"environment": "production" // Optional: target specific env
}],
"ai": { "binding": "AI" },
"browser": { "binding": "BROWSER" },
"workflows": [{ "binding": "MY_WORKFLOW", "name": "my-workflow" }]
}
```
**Create workflows:**
```bash
npx wrangler workflows create my-workflow
```
## Platform Bindings
```jsonc
{
"analytics_engine_datasets": [{ "binding": "ANALYTICS" }],
"mtls_certificates": [{ "binding": "MY_CERT", "certificate_id": "..." }],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "..." }],
"unsafe": {
"bindings": [{ "name": "RATE_LIMITER", "type": "ratelimit", "namespace_id": "..." }]
}
}
```
## Configuration Bindings
```jsonc
{
"vars": {
"API_URL": "https://api.example.com",
"MAX_RETRIES": "3"
},
"text_blobs": { "MY_TEXT": "./data/template.html" },
"data_blobs": { "MY_DATA": "./data/config.bin" },
"wasm_modules": { "MY_WASM": "./build/module.wasm" }
}
```
**Secrets (never in config):**
```bash
npx wrangler secret put API_KEY
```
## Environment-Specific Configuration
```jsonc
{
"name": "my-worker",
"vars": { "ENV": "production" },
"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
"env": {
"staging": {
"vars": { "ENV": "staging" },
"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }]
}
}
}
```
**Deploy:**
```bash
npx wrangler deploy # Production
npx wrangler deploy --env staging
```
## Local Development
```jsonc
{
"kv_namespaces": [{
"binding": "MY_KV",
"id": "prod-id",
"preview_id": "dev-id" // Used in wrangler dev
}]
}
```
**Or use remote:**
```bash
npx wrangler dev --remote # Uses production bindings
```
## Complete Example
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"vars": { "API_URL": "https://api.example.com" },
"kv_namespaces": [{ "binding": "CACHE", "id": "abc123" }],
"r2_buckets": [{ "binding": "ASSETS", "bucket_name": "my-assets" }],
"d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],
"services": [{ "binding": "AUTH", "service": "auth-worker" }],
"ai": { "binding": "AI" }
}
```
## Binding-Specific Configuration
### Durable Objects with Class Export
```jsonc
{
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter", "script_name": "my-worker" }
]
}
}
```
```typescript
// In same Worker or script_name Worker
export class Counter {
constructor(private state: DurableObjectState, private env: Env) {}
async fetch(request: Request) { /* ... */ }
}
```
### Queue Consumers
```jsonc
{
"queues": {
"producers": [{ "binding": "MY_QUEUE", "queue": "my-queue" }],
"consumers": [{ "queue": "my-queue", "max_batch_size": 10 }]
}
}
```
Queue consumer handler: `export default { async queue(batch, env) { /* process batch.messages */ } }`
## Key Points
- **64 binding limit** (all types combined)
- **Secrets**: Always use `wrangler secret put`, never commit
- **Types**: Run `npx wrangler types` after config changes
- **Environments**: Use `env` field for staging/production variants
- **Development**: Use `preview_id` or `--remote` flag
- **IDs vs Names**: Some bindings use `id` (KV, D1), others use `name` (R2, Queues)
## See Also
- [Wrangler Configuration](https://developers.cloudflare.com/workers/wrangler/configuration/)

View File

@@ -0,0 +1,208 @@
# Binding Gotchas and Troubleshooting
## Critical: Global Scope Mutation
### ❌ THE #1 GOTCHA: Caching env in Global Scope
```typescript
// ❌ DANGEROUS - env cached at deploy time
const apiKey = env.API_KEY; // ERROR: env not available in global scope
export default {
async fetch(request: Request, env: Env) {
// Uses undefined or stale value!
}
}
```
**Why it breaks:**
- `env` not available in global scope
- If using workarounds, secrets may not update without redeployment
- Leads to "Cannot read property 'X' of undefined" errors
**✅ Always access env per-request:**
```typescript
export default {
async fetch(request: Request, env: Env) {
const apiKey = env.API_KEY; // Fresh every request
}
}
```
## Common Errors
### "env.MY_KV is undefined"
**Cause:** Name mismatch or not configured
**Solution:** Check wrangler.jsonc (case-sensitive), run `npx wrangler types`, verify `npx wrangler kv namespace list`
### "Property 'MY_KV' does not exist on type 'Env'"
**Cause:** Types not generated
**Solution:** `npx wrangler types`
### "preview_id is required for --remote"
**Cause:** Missing preview binding
**Solution:** Add `"preview_id": "dev-id"` or use `npx wrangler dev` (local mode)
### "Secret updated but Worker still uses old value"
**Cause:** Cached in global scope or not redeployed
**Solution:** Avoid global caching, redeploy after secret change
### "KV get() returns null for existing key"
**Cause:** Eventual consistency (60s), wrong namespace, wrong environment
**Solution:**
```bash
# Check key exists
npx wrangler kv key get --binding=MY_KV "your-key"
# Verify namespace ID
npx wrangler kv namespace list
# Check environment
npx wrangler deployments list
```
### "D1 database not found"
**Solution:** `npx wrangler d1 list`, verify ID in wrangler.jsonc
### "Service binding returns 'No such service'"
**Cause:** Target Worker not deployed, name mismatch, environment mismatch
**Solution:**
```bash
# List deployed Workers
npx wrangler deployments list --name=target-worker
# Check service binding config
cat wrangler.jsonc | grep -A2 services
# Deploy target first
cd ../target-worker && npx wrangler deploy
```
### "Rate limit exceeded" on KV writes
**Cause:** >1 write/second per key
**Solution:** Use different keys, Durable Objects, or Queues
## Type Safety Gotchas
### Missing @cloudflare/workers-types
**Error:** `Cannot find name 'Request'`
**Solution:** `npm install -D @cloudflare/workers-types`, add to tsconfig.json `"types"`
### Binding Type Mismatches
```typescript
// ❌ Wrong - KV returns string | null
const value: string = await env.MY_KV.get('key');
// ✅ Handle null
const value = await env.MY_KV.get('key');
if (!value) return new Response('Not found', { status: 404 });
```
## Environment Gotchas
### Wrong Environment Deployed
**Solution:** Check `npx wrangler deployments list`, use `--env` flag
### Secrets Not Per-Environment
**Solution:** Set per environment: `npx wrangler secret put API_KEY --env staging`
## Development Gotchas
**wrangler dev vs deploy:**
- dev: Uses `preview_id` or local bindings, secrets not available
- deploy: Uses production `id`, secrets available
**Access secrets in dev:** `npx wrangler dev --remote`
**Persist local data:** `npx wrangler dev --persist`
## Performance Gotchas
### Sequential Binding Calls
```typescript
// ❌ Slow
const user = await env.DB.prepare('...').first();
const config = await env.MY_KV.get('config');
// ✅ Parallel
const [user, config] = await Promise.all([
env.DB.prepare('...').first(),
env.MY_KV.get('config')
]);
```
## Security Gotchas
**❌ Secrets in logs:** `console.log('Key:', env.API_KEY)` - visible in dashboard
**✅** `console.log('Key:', env.API_KEY ? '***' : 'missing')`
**❌ Exposing env:** `return Response.json(env)` - exposes all bindings
**✅** Never return env object in responses
## Limits Reference
| Resource | Limit | Impact | Plan |
|----------|-------|--------|------|
| **Bindings per Worker** | 64 total | All binding types combined | All |
| **Environment variables** | 64 max, 5KB each | Per Worker | All |
| **Secret size** | 1KB | Per secret | All |
| **KV key size** | 512 bytes | UTF-8 encoded | All |
| **KV value size** | 25 MB | Per value | All |
| **KV writes per key** | 1/second | Per key; exceeding = 429 error | All |
| **KV list() results** | 1000 keys | Per call; use cursor for more | All |
| **KV operations** | 1000 reads/day | Free tier only | Free |
| **R2 object size** | 5 TB | Per object | All |
| **R2 operations** | 1M Class A/month free | Writes | All |
| **D1 database size** | 10 GB | Per database | All |
| **D1 rows per query** | 100,000 | Result set limit | All |
| **D1 databases** | 10 | Free tier | Free |
| **Queue batch size** | 100 messages | Per consumer batch | All |
| **Queue message size** | 128 KB | Per message | All |
| **Service binding calls** | Unlimited | Counts toward CPU time | All |
| **Durable Objects** | 1M requests/month free | First 1M | Free |
## Debugging Tips
```bash
# Check configuration
npx wrangler deploy --dry-run # Validate config without deploying
npx wrangler kv namespace list # List KV namespaces
npx wrangler secret list # List secrets (not values)
npx wrangler deployments list # Recent deployments
# Inspect bindings
npx wrangler kv key list --binding=MY_KV
npx wrangler kv key get --binding=MY_KV "key-name"
npx wrangler r2 object get my-bucket/file.txt
npx wrangler d1 execute my-db --command="SELECT * FROM sqlite_master"
# Test locally
npx wrangler dev # Local mode
npx wrangler dev --remote # Production bindings
npx wrangler dev --persist # Persist data across restarts
# Verify types
npx wrangler types
cat .wrangler/types/runtime.d.ts | grep "interface Env"
# Debug specific binding issues
npx wrangler tail # Stream logs in real-time
npx wrangler tail --format=pretty # Formatted logs
```
## See Also
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Wrangler Commands](https://developers.cloudflare.com/workers/wrangler/commands/)

View File

@@ -0,0 +1,200 @@
# Binding Patterns and Best Practices
## Service Binding Patterns
### RPC via Service Bindings
```typescript
// auth-worker
export default {
async fetch(request: Request, env: Env) {
const token = request.headers.get('Authorization');
return new Response(JSON.stringify({ valid: await validateToken(token) }));
}
}
// api-worker
const response = await env.AUTH_SERVICE.fetch(
new Request('https://fake-host/validate', {
headers: { 'Authorization': token }
})
);
```
**Why RPC?** Zero latency (same datacenter), no DNS, free, type-safe.
**HTTP vs Service:**
```typescript
// ❌ HTTP (slow, paid, cross-region latency)
await fetch('https://auth-worker.example.com/validate');
// ✅ Service binding (fast, free, same isolate)
await env.AUTH_SERVICE.fetch(new Request('https://fake-host/validate'));
```
**URL doesn't matter:** Service bindings ignore hostname/protocol, routing happens via binding name.
### Typed Service RPC
```typescript
// shared-types.ts
export interface AuthRequest { token: string; }
export interface AuthResponse { valid: boolean; userId?: string; }
// auth-worker
export default {
async fetch(request: Request): Promise<Response> {
const body: AuthRequest = await request.json();
const response: AuthResponse = { valid: true, userId: '123' };
return Response.json(response);
}
}
// api-worker
const response = await env.AUTH_SERVICE.fetch(
new Request('https://fake/validate', {
method: 'POST',
body: JSON.stringify({ token } satisfies AuthRequest)
})
);
const data: AuthResponse = await response.json();
```
## Secrets Management
```bash
# Set secret
npx wrangler secret put API_KEY
cat api-key.txt | npx wrangler secret put API_KEY
npx wrangler secret put API_KEY --env staging
```
```typescript
// Use secret
const response = await fetch('https://api.example.com', {
headers: { 'Authorization': `Bearer ${env.API_KEY}` }
});
```
**Never commit secrets:**
```jsonc
// ❌ NEVER
{ "vars": { "API_KEY": "sk_live_abc123" } }
```
## Testing with Mock Bindings
### Vitest Mock
```typescript
import { vi } from 'vitest';
const mockKV: KVNamespace = {
get: vi.fn(async (key) => key === 'test' ? 'value' : null),
put: vi.fn(async () => {}),
delete: vi.fn(async () => {}),
list: vi.fn(async () => ({ keys: [], list_complete: true, cursor: '' })),
getWithMetadata: vi.fn(),
} as unknown as KVNamespace;
const mockEnv: Env = { MY_KV: mockKV };
const mockCtx: ExecutionContext = {
waitUntil: vi.fn(),
passThroughOnException: vi.fn(),
};
const response = await worker.fetch(
new Request('http://localhost/test'),
mockEnv,
mockCtx
);
```
## Binding Access Patterns
### Lazy Access
```typescript
// ✅ Access only when needed
if (url.pathname === '/cached') {
const cached = await env.MY_KV.get('data');
if (cached) return new Response(cached);
}
```
### Parallel Access
```typescript
// ✅ Parallelize independent calls
const [user, config, cache] = await Promise.all([
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first(),
env.MY_KV.get('config'),
env.CACHE.get('data')
]);
```
## Storage Selection
### KV: CDN-Backed Reads
```typescript
const config = await env.MY_KV.get('app-config', { type: 'json' });
```
**Use when:** Read-heavy, <25MB, global distribution, eventual consistency OK
**Latency:** <10ms reads (cached), writes eventually consistent (60s)
### D1: Relational Queries
```typescript
const results = await env.DB.prepare(`
SELECT u.name, COUNT(o.id) FROM users u
LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id
`).all();
```
**Use when:** Relational data, JOINs, ACID transactions
**Limits:** 10GB database size, 100k rows per query
### R2: Large Objects
```typescript
const object = await env.MY_BUCKET.get('large-file.zip');
return new Response(object.body);
```
**Use when:** Files >25MB, S3-compatible API needed
**Limits:** 5TB per object, unlimited storage
### Durable Objects: Coordination
```typescript
const id = env.COUNTER.idFromName('global');
const stub = env.COUNTER.get(id);
await stub.fetch(new Request('https://fake/increment'));
```
**Use when:** Strong consistency, real-time coordination, WebSocket state
**Guarantees:** Single-threaded execution, transactional storage
## Anti-Patterns
**❌ Hardcoding credentials:** `const apiKey = 'sk_live_abc123'`
**✅** `npx wrangler secret put API_KEY`
**❌ Using REST API:** `fetch('https://api.cloudflare.com/.../kv/...')`
**✅** `env.MY_KV.get('key')`
**❌ Polling storage:** `setInterval(() => env.KV.get('config'), 1000)`
**✅** Use Durable Objects for real-time state
**❌ Large data in vars:** `{ "vars": { "HUGE_CONFIG": "..." } }` (5KB max)
**✅** `env.MY_KV.put('config', data)`
**❌ Caching env globally:** `const apiKey = env.API_KEY` outside fetch()
**✅** Access `env.API_KEY` per-request inside fetch()
## See Also
- [Service Bindings Docs](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/)
- [Miniflare Testing](https://miniflare.dev/)

View File

@@ -0,0 +1,94 @@
# Cloudflare Bot Management
Enterprise-grade bot detection, protection, and mitigation using ML/heuristics, bot scores, JavaScript detections, and verified bot handling.
## Overview
Bot Management provides multi-tier protection:
- **Free (Bot Fight Mode)**: Auto-blocks definite bots, no config
- **Pro/Business (Super Bot Fight Mode)**: Configurable actions, static resource protection, analytics groupings
- **Enterprise (Bot Management)**: Granular 1-99 scores, WAF integration, JA3/JA4 fingerprinting, Workers API, Advanced Analytics
## Quick Start
```txt
# Dashboard: Security > Bots
# Enterprise: Deploy rule template
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot) → Block
(cf.bot_management.score le 29 and not cf.bot_management.verified_bot) → Managed Challenge
```
## What Do You Need?
```txt
├─ Initial setup → configuration.md
│ ├─ Free tier → "Bot Fight Mode"
│ ├─ Pro/Business → "Super Bot Fight Mode"
│ └─ Enterprise → "Bot Management for Enterprise"
├─ Workers API integration → api.md
├─ WAF rules → patterns.md
├─ Debugging → gotchas.md
└─ Analytics → api.md#bot-analytics
```
## Reading Order
| Task | Files to Read |
|------|---------------|
| Enable bot protection | README → configuration.md |
| Workers bot detection | README → api.md |
| WAF rule templates | README → patterns.md |
| Debug bot issues | gotchas.md |
| Advanced analytics | api.md#bot-analytics |
## Core Concepts
**Bot Scores**: 1-99 (1 = definitely automated, 99 = definitely human). Threshold: <30 indicates bot traffic. Enterprise gets granular 1-99; Pro/Business get groupings only.
**Detection Engines**: Heuristics (known fingerprints, assigns score=1), ML (majority of detections, supervised learning on billions of requests), Anomaly Detection (optional, baseline traffic analysis), JavaScript Detections (headless browser detection).
**Verified Bots**: Allowlisted good bots (search engines, AI crawlers) verified via reverse DNS or Web Bot Auth. Access via `cf.bot_management.verified_bot` or `cf.verified_bot_category`.
## Platform Limits
| Plan | Bot Scores | JA3/JA4 | Custom Rules | Analytics Retention |
|------|------------|---------|--------------|---------------------|
| Free | No (auto-block only) | No | 5 | N/A (no analytics) |
| Pro/Business | Groupings only | No | 20/100 | 30 days (72h at a time) |
| Enterprise | 1-99 granular | Yes | 1,000+ | 30 days (1 week at a time) |
## Basic Patterns
```typescript
// Workers: Check bot score
export default {
async fetch(request: Request): Promise<Response> {
const botScore = request.cf?.botManagement?.score;
if (botScore && botScore < 30 && !request.cf?.botManagement?.verifiedBot) {
return new Response('Bot detected', { status: 403 });
}
return fetch(request);
}
};
```
```txt
# WAF: Block definite bots
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot)
# WAF: Protect sensitive endpoints
(cf.bot_management.score lt 50 and http.request.uri.path in {"/login" "/checkout"} and not cf.bot_management.verified_bot)
```
## In This Reference
- [configuration.md](./configuration.md) - Product tiers, WAF rule setup, JavaScript Detections, ML auto-updates
- [api.md](./api.md) - Workers BotManagement interface, WAF fields, JA4 Signals
- [patterns.md](./patterns.md) - E-commerce, API protection, mobile app allowlisting, SEO-friendly handling
- [gotchas.md](./gotchas.md) - False positives/negatives, score=0 issues, JSD limitations, CSP requirements
## See Also
- [waf](../waf/) - WAF custom rules for bot enforcement
- [workers](../workers/) - Workers request.cf.botManagement API
- [api-shield](../api-shield/) - API-specific bot protection

View File

@@ -0,0 +1,169 @@
# Bot Management API
## Workers: BotManagement Interface
```typescript
interface BotManagement {
score: number; // 1-99 (Enterprise), 0 if not computed
verifiedBot: boolean; // Is verified bot
staticResource: boolean; // Serves static resource
ja3Hash: string; // JA3 fingerprint (Enterprise, HTTPS only)
ja4: string; // JA4 fingerprint (Enterprise, HTTPS only)
jsDetection?: {
passed: boolean; // Passed JS detection (if enabled)
};
detectionIds: number[]; // Heuristic detection IDs
corporateProxy?: boolean; // From corporate proxy (Enterprise)
}
// DEPRECATED: Use botManagement.score instead
// request.cf.clientTrustScore (legacy, duplicate of botManagement.score)
// Access via request.cf
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (!botMgmt) return fetch(request);
if (botMgmt.verifiedBot) return fetch(request); // Allow verified bots
if (botMgmt.score === 1) return new Response('Blocked', { status: 403 });
if (botMgmt.score < 30) return new Response('Challenge required', { status: 429 });
return fetch(request);
}
};
```
## WAF Fields Reference
```txt
# Score fields
cf.bot_management.score # 0-99 (0 = not computed)
cf.bot_management.verified_bot # boolean
cf.bot_management.static_resource # boolean
cf.bot_management.ja3_hash # string (Enterprise)
cf.bot_management.ja4 # string (Enterprise)
cf.bot_management.detection_ids # array
cf.bot_management.js_detection.passed # boolean
cf.bot_management.corporate_proxy # boolean (Enterprise)
cf.verified_bot_category # string
# Workers equivalent
request.cf.botManagement.score
request.cf.botManagement.verifiedBot
request.cf.botManagement.ja3Hash
request.cf.botManagement.ja4
request.cf.botManagement.jsDetection.passed
request.cf.verifiedBotCategory
```
## JA4 Signals (Enterprise)
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
interface JA4Signals {
// Ratios (0.0-1.0)
heuristic_ratio_1h?: number; // Fraction flagged by heuristics
browser_ratio_1h?: number; // Fraction from real browsers
cache_ratio_1h?: number; // Fraction hitting cache
h2h3_ratio_1h?: number; // Fraction using HTTP/2 or HTTP/3
// Ranks (relative position in distribution)
uas_rank_1h?: number; // User-Agent diversity rank
paths_rank_1h?: number; // Path diversity rank
reqs_rank_1h?: number; // Request volume rank
ips_rank_1h?: number; // IP diversity rank
// Quantiles (0.0-1.0, percentile in distribution)
reqs_quantile_1h?: number; // Request volume quantile
ips_quantile_1h?: number; // IP count quantile
}
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const ja4Signals = cf?.ja4Signals as JA4Signals | undefined;
if (!ja4Signals) return fetch(request); // Not available for HTTP or Worker routing
// Check for anomalous behavior
// High heuristic_ratio or low browser_ratio = suspicious
const heuristicRatio = ja4Signals.heuristic_ratio_1h ?? 0;
const browserRatio = ja4Signals.browser_ratio_1h ?? 0;
if (heuristicRatio > 0.5 || browserRatio < 0.3) {
return new Response('Suspicious traffic', { status: 403 });
}
return fetch(request);
}
};
```
## Common Patterns
See [patterns.md](./patterns.md) for Workers examples: mobile app allowlisting, corporate proxy exemption, datacenter detection, conditional delay, and more.
## Bot Analytics
### Access Locations
- Dashboard: Security > Bots (old) or Security > Analytics > Bot analysis (new)
- GraphQL API for programmatic access
- Security Events & Security Analytics
- Logpush/Logpull
### Available Data
- **Enterprise BM**: Bot scores (1-99), bot score source, distribution
- **Pro/Business**: Bot groupings (automated, likely automated, likely human)
- Top attributes: IPs, paths, user agents, countries
- Detection sources: Heuristics, ML, AD, JSD
- Verified bot categories
### Time Ranges
- **Enterprise BM**: Up to 1 week at a time, 30 days history
- **Pro/Business**: Up to 72 hours at a time, 30 days history
- Real-time in most cases, adaptive sampling (1-10% depending on volume)
## Logpush Fields
```txt
BotScore # 1-99 or 0 if not computed
BotScoreSrc # Detection engine (ML, Heuristics, etc.)
BotTags # Classification tags
BotDetectionIDs # Heuristic detection IDs
```
**BotScoreSrc values:**
- `"Heuristics"` - Known fingerprint
- `"Machine Learning"` - ML model
- `"Anomaly Detection"` - Baseline anomaly
- `"JS Detection"` - JavaScript check
- `"Cloudflare Service"` - Zero Trust
- `"Not Computed"` - Score = 0
Access via Logpush (stream to cloud storage/SIEM), Logpull (API to fetch logs), or GraphQL API (query analytics data).
## Testing with Miniflare
Miniflare provides mock botManagement data for local development:
**Default values:**
- `score: 99` (human)
- `verifiedBot: false`
- `corporateProxy: false`
- `ja3Hash: "25b4882c2bcb50cd6b469ff28c596742"`
- `staticResource: false`
- `detectionIds: []`
**Override in tests:**
```typescript
import { getPlatformProxy } from 'wrangler';
const { cf, dispose } = await getPlatformProxy();
// cf.botManagement is frozen mock object
expect(cf.botManagement.score).toBe(99);
```
For custom test data, mock request.cf in your test setup.

View File

@@ -0,0 +1,163 @@
# Bot Management Configuration
## Product Tiers
**Note:** Dashboard paths differ between old and new UI:
- **New:** Security > Settings > Filter "Bot traffic"
- **Old:** Security > Bots
Both UIs access same settings.
### Bot Score Groupings (Pro/Business)
Pro/Business users see bot score groupings instead of granular 1-99 scores:
| Score | Grouping | Meaning |
|-------|----------|---------|
| 0 | Not computed | Bot Management didn't run |
| 1 | Automated | Definite bot (heuristic match) |
| 2-29 | Likely automated | Probably bot (ML detection) |
| 30-99 | Likely human | Probably human |
| N/A | Verified bot | Allowlisted good bot |
Enterprise plans get granular 1-99 scores for custom thresholds.
### Bot Fight Mode (Free)
- Auto-blocks definite bots (score=1), excludes verified bots by default
- JavaScript Detections always enabled, no configuration options
### Super Bot Fight Mode (Pro/Business)
```txt
Dashboard: Security > Bots > Configure
- Definitely automated: Block/Challenge
- Likely automated: Challenge/Allow
- Verified bots: Allow (recommended)
- Static resource protection: ON (may block mail clients)
- JavaScript Detections: Optional
```
### Bot Management for Enterprise
```txt
Dashboard: Security > Bots > Configure > Auto-updates: ON (recommended)
# Template 1: Block definite bots
(cf.bot_management.score eq 1 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
Action: Block
# Template 2: Challenge likely bots
(cf.bot_management.score ge 2 and cf.bot_management.score le 29 and not cf.bot_management.verified_bot and not cf.bot_management.static_resource)
Action: Managed Challenge
```
## JavaScript Detections Setup
### Enable via Dashboard
```txt
Security > Bots > Configure Bot Management > JS Detections: ON
Update CSP: script-src 'self' /cdn-cgi/challenge-platform/;
```
### Manual JS Injection (API)
```html
<script>
function jsdOnload() {
window.cloudflare.jsd.executeOnce({ callback: function(result) { console.log('JSD:', result); } });
}
</script>
<script src="/cdn-cgi/challenge-platform/scripts/jsd/api.js?onload=jsdOnload" async></script>
```
**Use API for**: Selective deployment on specific pages
**Don't combine**: Zone-wide toggle + manual injection
### WAF Rules for JSD
```txt
# NEVER use on first page visit (needs HTML page first)
(not cf.bot_management.js_detection.passed and http.request.uri.path eq "/api/user/create" and http.request.method eq "POST" and not cf.bot_management.verified_bot)
Action: Managed Challenge (always use Managed Challenge, not Block)
```
### Limitations
- First request won't have JSD data (needs HTML page first)
- Strips ETags from HTML responses
- Not supported with CSP via `<meta>` tags
- Websocket endpoints not supported
- Native mobile apps won't pass
- cf_clearance cookie: 15-minute lifespan, max 4096 bytes
## __cf_bm Cookie
Cloudflare sets `__cf_bm` cookie to smooth bot scores across user sessions:
- **Purpose:** Reduces false positives from score volatility
- **Scope:** Per-domain, HTTP-only
- **Lifespan:** Session duration
- **Privacy:** No PII—only session classification
- **Automatic:** No configuration required
Bot scores for repeat visitors consider session history via this cookie.
## Static Resource Protection
**File Extensions**: ico, jpg, png, jpeg, gif, css, js, tif, tiff, bmp, pict, webp, svg, svgz, class, jar, txt, csv, doc, docx, xls, xlsx, pdf, ps, pls, ppt, pptx, ttf, otf, woff, woff2, eot, eps, ejs, swf, torrent, midi, mid, m3u8, m4a, mp3, ogg, ts
**Plus**: `/.well-known/` path (all files)
```txt
# Exclude static resources from bot rules
(cf.bot_management.score lt 30 and not cf.bot_management.static_resource)
```
**WARNING**: May block mail clients fetching static images
## JA3/JA4 Fingerprinting (Enterprise)
```txt
# Block specific attack fingerprint
(cf.bot_management.ja3_hash eq "8b8e3d5e3e8b3d5e")
# Allow mobile app by fingerprint
(cf.bot_management.ja4 eq "your_mobile_app_fingerprint")
```
Only available for HTTPS/TLS traffic. Missing for Worker-routed traffic or HTTP requests.
## Verified Bot Categories
```txt
# Allow search engines only
(cf.verified_bot_category eq "Search Engine Crawler")
# Block AI crawlers
(cf.verified_bot_category eq "AI Crawler")
Action: Block
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
```
| Category | String Value | Example |
|----------|--------------|---------|
| AI Crawler | `AI Crawler` | GPTBot, Claude-Web |
| AI Assistant | `AI Assistant` | Perplexity-User, DuckAssistBot |
| AI Search | `AI Search` | OAI-SearchBot |
| Accessibility | `Accessibility` | Accessible Web Bot |
| Academic Research | `Academic Research` | Library of Congress |
| Advertising & Marketing | `Advertising & Marketing` | Google Adsbot |
| Aggregator | `Aggregator` | Pinterest, Indeed |
| Archiver | `Archiver` | Internet Archive, CommonCrawl |
| Feed Fetcher | `Feed Fetcher` | RSS/Podcast updaters |
| Monitoring & Analytics | `Monitoring & Analytics` | Uptime monitors |
| Page Preview | `Page Preview` | Facebook/Slack link preview |
| SEO | `Search Engine Optimization` | Google Lighthouse |
| Security | `Security` | Vulnerability scanners |
| Social Media Marketing | `Social Media Marketing` | Brandwatch |
| Webhooks | `Webhooks` | Payment processors |
| Other | `Other` | Uncategorized bots |
## Best Practices
- **ML Auto-Updates**: Enable on Enterprise for latest models
- **Start with Managed Challenge**: Test before blocking
- **Always exclude verified bots**: Use `not cf.bot_management.verified_bot`
- **Exempt corporate proxies**: For B2B traffic via `cf.bot_management.corporate_proxy`
- **Use static resource exception**: Improves performance, reduces overhead

View File

@@ -0,0 +1,114 @@
# Bot Management Gotchas
## Common Errors
### "Bot Score = 0"
**Cause:** Bot Management didn't run (internal Cloudflare request, Worker routing to zone (Orange-to-Orange), or request handled before BM (Redirect Rules, etc.))
**Solution:** Check request flow and ensure Bot Management runs in request lifecycle
### "JavaScript Detections Not Working"
**Cause:** `js_detection.passed` always false or undefined due to: CSP headers don't allow `/cdn-cgi/challenge-platform/`, using on first page visit (needs HTML page first), ad blockers or disabled JS, JSD not enabled in dashboard, or using Block action (must use Managed Challenge)
**Solution:** Add CSP header `Content-Security-Policy: script-src 'self' /cdn-cgi/challenge-platform/;` and ensure JSD is enabled with Managed Challenge action
### "False Positives (Legitimate Users Blocked)"
**Cause:** Bot detection incorrectly flagging legitimate users
**Solution:** Check Bot Analytics for affected IPs/paths, identify detection source (ML, Heuristics, etc.), create exception rule like `(cf.bot_management.score lt 30 and http.request.uri.path eq "/problematic-path")` with Action: Skip (Bot Management), or allowlist by IP/ASN/country
### "False Negatives (Bots Not Caught)"
**Cause:** Bots bypassing detection
**Solution:** Lower score threshold (30 → 50), enable JavaScript Detections, add JA3/JA4 fingerprinting rules, or use rate limiting as fallback
### "Verified Bot Blocked"
**Cause:** Search engine bot blocked by WAF Managed Rules (not just Bot Management)
**Solution:** Create WAF exception for specific rule ID and verify bot via reverse DNS
### "Yandex Bot Blocked During IP Update"
**Cause:** Yandex updates bot IPs; new IPs unrecognized for 48h during propagation
**Solution:**
1. Check Security Events for specific WAF rule ID blocking Yandex
2. Create WAF exception:
```txt
(http.user_agent contains "YandexBot" and ip.src in {<yandex-ip-range>})
Action: Skip (WAF Managed Ruleset)
```
3. Monitor Bot Analytics for 48h
4. Remove exception after propagation completes
Issue resolves automatically after 48h. Contact Cloudflare Support if persists.
### "JA3/JA4 Missing"
**Cause:** Non-HTTPS traffic, Worker routing traffic, Orange-to-Orange traffic via Worker, or Bot Management skipped
**Solution:** JA3/JA4 only available for HTTPS/TLS traffic; check request routing
**JA3/JA4 Not User-Unique:** Same browser/library version = same fingerprint
- Don't use for user identification
- Use for client profiling only
- Fingerprints change with browser updates
## Bot Verification Methods
Cloudflare verifies bots via:
1. **Reverse DNS (IP validation):** Traditional method—bot IP resolves to expected domain
2. **Web Bot Auth:** Modern cryptographic verification—faster propagation
When `verifiedBot=true`, bot passed at least one method.
**Inactive verified bots:** IPs removed after 24h of no traffic.
## Detection Engine Behavior
| Engine | Score | Timing | Plan | Notes |
|--------|-------|--------|------|-------|
| Heuristics | Always 1 | Immediate | All | Known fingerprints—overrides ML |
| ML | 1-99 | Immediate | All | Majority of detections |
| Anomaly Detection | Influences | After baseline | Enterprise | Optional, baseline analysis |
| JavaScript Detections | Pass/fail | After JS | Pro+ | Headless browser detection |
| Cloudflare Service | N/A | N/A | Enterprise | Zero Trust internal source |
**Priority:** Heuristics > ML—if heuristic matches, score=1 regardless of ML.
## Limits
| Limit | Value | Notes |
|-------|-------|-------|
| Bot Score = 0 | Means not computed | Not score = 100 |
| First request JSD data | May not be available | JSD data appears on subsequent requests |
| Score accuracy | Not 100% guaranteed | False positives/negatives possible |
| JSD on first HTML page visit | Not supported | Requires subsequent page load |
| JSD requirements | JavaScript-enabled browser | Won't work with JS disabled or ad blockers |
| JSD ETag stripping | Strips ETags from HTML responses | May affect caching behavior |
| JSD CSP compatibility | Requires specific CSP | Not compatible with some CSP configurations |
| JSD meta CSP tags | Not supported | Must use HTTP headers |
| JSD WebSocket support | Not supported | WebSocket endpoints won't work with JSD |
| JSD mobile app support | Native apps won't pass | Only works in browsers |
| JA3/JA4 traffic type | HTTPS/TLS only | Not available for non-HTTPS traffic |
| JA3/JA4 Worker routing | Missing for Worker-routed traffic | Check request routing |
| JA3/JA4 uniqueness | Not unique per user | Shared by clients with same browser/library |
| JA3/JA4 stability | Can change with updates | Browser/library updates affect fingerprints |
| WAF custom rules (Free) | 5 | Varies by plan |
| WAF custom rules (Pro) | 20 | Varies by plan |
| WAF custom rules (Business) | 100 | Varies by plan |
| WAF custom rules (Enterprise) | 1,000+ | Varies by plan |
| Workers CPU time | Varies by plan | Applies to bot logic |
| Bot Analytics sampling | 1-10% adaptive | High-volume zones sampled more aggressively |
| Bot Analytics history | 30 days max | Historical data retention limit |
| CSP requirements for JSD | Must allow `/cdn-cgi/challenge-platform/` | Required for JSD to function |
### Plan Restrictions
| Feature | Free | Pro/Business | Enterprise |
|---------|------|--------------|------------|
| Granular scores (1-99) | No | No | Yes |
| JA3/JA4 | No | No | Yes |
| Anomaly Detection | No | No | Yes |
| Corporate Proxy detection | No | No | Yes |
| Verified bot categories | Limited | Limited | Full |
| Custom WAF rules | 5 | 20/100 | 1,000+ |

View File

@@ -0,0 +1,182 @@
# Bot Management Patterns
## E-commerce Protection
```txt
# High security for checkout
(cf.bot_management.score lt 50 and http.request.uri.path in {"/checkout" "/cart/add"} and not cf.bot_management.verified_bot and not cf.bot_management.corporate_proxy)
Action: Managed Challenge
```
## API Protection
```txt
# Protect API with JS detection + score
(http.request.uri.path matches "^/api/" and (cf.bot_management.score lt 30 or not cf.bot_management.js_detection.passed) and not cf.bot_management.verified_bot)
Action: Block
```
## SEO-Friendly Bot Handling
```txt
# Allow search engine crawlers
(cf.bot_management.score lt 30 and not cf.verified_bot_category in {"Search Engine Crawler"})
Action: Managed Challenge
```
## Block AI Scrapers
```txt
# Block training crawlers only (allow AI assistants/search)
(cf.verified_bot_category eq "AI Crawler")
Action: Block
# Block all AI-related bots (training + assistants + search)
(cf.verified_bot_category in {"AI Crawler" "AI Assistant" "AI Search"})
Action: Block
# Allow AI Search, block AI Crawler and AI Assistant
(cf.verified_bot_category in {"AI Crawler" "AI Assistant"})
Action: Block
# Or use dashboard: Security > Settings > Bot Management > Block AI Bots
```
## Rate Limiting by Bot Score
```txt
# Stricter limits for suspicious traffic
(cf.bot_management.score lt 50)
Rate: 10 requests per 10 seconds
(cf.bot_management.score ge 50)
Rate: 100 requests per 10 seconds
```
## Mobile App Allowlisting
```txt
# Identify mobile app by JA3/JA4
(cf.bot_management.ja4 in {"fingerprint1" "fingerprint2"})
Action: Skip (all remaining rules)
```
## Datacenter Detection
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
// Low score + not corporate proxy = likely datacenter bot
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (botMgmt?.score && botMgmt.score < 30 &&
!botMgmt.corporateProxy && !botMgmt.verifiedBot) {
return new Response('Datacenter traffic blocked', { status: 403 });
}
return fetch(request);
}
};
```
## Conditional Delay (Tarpit)
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
// Add delay proportional to bot suspicion
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
if (botMgmt?.score && botMgmt.score < 50 && !botMgmt.verifiedBot) {
// Delay: 0-2 seconds for scores 50-0
const delayMs = Math.max(0, (50 - botMgmt.score) * 40);
await new Promise(r => setTimeout(r, delayMs));
}
return fetch(request);
}
};
```
## Layered Defense
```txt
1. Bot Management (score-based)
2. JavaScript Detections (for JS-capable clients)
3. Rate Limiting (fallback protection)
4. WAF Managed Rules (OWASP, etc.)
```
## Progressive Enhancement
```txt
Public content: High threshold (score < 10)
Authenticated: Medium threshold (score < 30)
Sensitive: Low threshold (score < 50) + JSD
```
## Zero Trust for Bots
```txt
1. Default deny (all scores < 30)
2. Allowlist verified bots
3. Allowlist mobile apps (JA3/JA4)
4. Allowlist corporate proxies
5. Allowlist static resources
```
## Workers: Score + JS Detection
```typescript
import type { IncomingRequestCfProperties } from '@cloudflare/workers-types';
export default {
async fetch(request: Request): Promise<Response> {
const cf = request.cf as IncomingRequestCfProperties | undefined;
const botMgmt = cf?.botManagement;
const url = new URL(request.url);
if (botMgmt?.staticResource) return fetch(request); // Skip static
// API endpoints: require JS detection + good score
if (url.pathname.startsWith('/api/')) {
const jsDetectionPassed = botMgmt?.jsDetection?.passed ?? false;
const score = botMgmt?.score ?? 100;
if (!jsDetectionPassed || score < 30) {
return new Response('Unauthorized', { status: 401 });
}
}
return fetch(request);
}
};
```
## Rate Limiting by JWT Claim + Bot Score
```txt
# Enterprise: Combine bot score with JWT validation
Rate limiting > Custom rules
- Field: lookup_json_string(http.request.jwt.claims["{config_id}"][0], "sub")
- Matches: user ID claim
- Additional condition: cf.bot_management.score lt 50
```
## WAF Integration Points
- **WAF Custom Rules**: Primary enforcement mechanism
- **Rate Limiting Rules**: Bot score as dimension, stricter limits for low scores
- **Transform Rules**: Pass score to origin via custom header
- **Workers**: Programmatic bot logic, custom scoring algorithms
- **Page Rules / Configuration Rules**: Zone-level overrides, path-specific settings
## See Also
- [gotchas.md](./gotchas.md) - Common errors, false positives/negatives, limitations

View File

@@ -0,0 +1,78 @@
# Cloudflare Browser Rendering Skill Reference
**Description**: Expert knowledge for Cloudflare Browser Rendering - control headless Chrome on Cloudflare's global network for browser automation, screenshots, PDFs, web scraping, testing, and content generation.
**When to use**: Any task involving Cloudflare Browser Rendering including: taking screenshots, generating PDFs, web scraping, browser automation, testing web applications, extracting structured data, capturing page metrics, or automating browser interactions.
## Decision Tree
### REST API vs Workers Bindings
**Use REST API when:**
- One-off, stateless tasks (screenshot, PDF, content fetch)
- No Workers infrastructure yet
- Simple integrations from external services
- Need quick prototyping without deployment
**Use Workers Bindings when:**
- Complex browser automation workflows
- Need session reuse for performance
- Multiple page interactions per request
- Custom scripting and logic required
- Building production applications
### Puppeteer vs Playwright
| Feature | Puppeteer | Playwright |
|---------|-----------|------------|
| API Style | Chrome DevTools Protocol | High-level abstractions |
| Selectors | CSS, XPath | CSS, text, role, test-id |
| Best for | Advanced control, CDP access | Quick automation, testing |
| Learning curve | Steeper | Gentler |
**Use Puppeteer:** Need CDP protocol access, Chrome-specific features, migration from existing Puppeteer code
**Use Playwright:** Modern selector APIs, cross-browser patterns, faster development
## Tier Limits Summary
| Limit | Free Tier | Paid Tier |
|-------|-----------|-----------|
| Daily browser time | 10 minutes | Unlimited* |
| Concurrent sessions | 3 | 30 |
| Requests per minute | 6 | 180 |
*Subject to fair-use policy. See [gotchas.md](gotchas.md) for details.
## Reading Order
**New to Browser Rendering:**
1. [configuration.md](configuration.md) - Setup and deployment
2. [patterns.md](patterns.md) - Common use cases with examples
3. [api.md](api.md) - API reference
4. [gotchas.md](gotchas.md) - Avoid common pitfalls
**Specific task:**
- **Setup/deployment** → [configuration.md](configuration.md)
- **API reference/endpoints** → [api.md](api.md)
- **Example code/patterns** → [patterns.md](patterns.md)
- **Debugging/troubleshooting** → [gotchas.md](gotchas.md)
**REST API users:**
- Start with [api.md](api.md) REST API section
- Check [gotchas.md](gotchas.md) for rate limits
**Workers users:**
- Start with [configuration.md](configuration.md)
- Review [patterns.md](patterns.md) for session management
- Reference [api.md](api.md) for Workers Bindings
## In This Reference
- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config, compatibility
- **[api.md](api.md)** - REST API endpoints + Workers Bindings (Puppeteer/Playwright)
- **[patterns.md](patterns.md)** - Common patterns, use cases, real examples
- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, tier limits, common errors
## See Also
- [Cloudflare Docs](https://developers.cloudflare.com/browser-rendering/)

View File

@@ -0,0 +1,108 @@
# Browser Rendering API
## REST API
**Base:** `https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering`
**Auth:** `Authorization: Bearer <token>` (Browser Rendering - Edit permission)
### Endpoints
| Endpoint | Description | Key Options |
|----------|-------------|-------------|
| `/content` | Get rendered HTML | `url`, `waitUntil` |
| `/screenshot` | Capture image | `screenshotOptions: {type, fullPage, clip}` |
| `/pdf` | Generate PDF | `pdfOptions: {format, landscape, margin}` |
| `/snapshot` | HTML + inlined resources | `url` |
| `/scrape` | Extract by selectors | `selectors: ["h1", ".price"]` |
| `/json` | AI-structured extraction | `schema: {name: "string", price: "number"}` |
| `/links` | Get all links | `url` |
| `/markdown` | Convert to markdown | `url` |
```bash
curl -X POST '.../browser-rendering/screenshot' \
-H "Authorization: Bearer $TOKEN" \
-d '{"url":"https://example.com","screenshotOptions":{"fullPage":true}}'
```
## Workers Binding
```jsonc
// wrangler.jsonc
{ "browser": { "binding": "MYBROWSER" } }
```
## Puppeteer
```typescript
import puppeteer from "@cloudflare/puppeteer";
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
// Content
const html = await page.content();
const title = await page.title();
// Screenshot/PDF
await page.screenshot({ fullPage: true, type: 'png' });
await page.pdf({ format: 'A4', printBackground: true });
// Interaction
await page.click('#button');
await page.type('#input', 'text');
await page.evaluate(() => document.querySelector('h1')?.textContent);
// Session management
const sessions = await puppeteer.sessions(env.MYBROWSER);
const limits = await puppeteer.limits(env.MYBROWSER);
await browser.close();
```
## Playwright
```typescript
import { launch, connect } from "@cloudflare/playwright";
const browser = await launch(env.MYBROWSER, { keep_alive: 600000 });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Modern selectors
await page.locator('.button').click();
await page.getByText('Submit').click();
await page.getByTestId('search').fill('query');
// Context for isolation
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'custom'
});
await browser.close();
```
## Session Management
```typescript
// List sessions
await puppeteer.sessions(env.MYBROWSER);
// Connect to existing
await puppeteer.connect(env.MYBROWSER, sessionId);
// Check limits
await puppeteer.limits(env.MYBROWSER);
// { remaining: ms, total: ms, concurrent: n }
```
## Key Options
| Option | Values |
|--------|--------|
| `waitUntil` | `load`, `domcontentloaded`, `networkidle0`, `networkidle2` |
| `keep_alive` | Max 600000ms (10 min) |
| `screenshot.type` | `png`, `jpeg` |
| `pdf.format` | `A4`, `Letter`, `Legal` |

View File

@@ -0,0 +1,78 @@
# Configuration & Setup
## Installation
```bash
npm install @cloudflare/puppeteer # or @cloudflare/playwright
```
**Use Cloudflare packages** - standard `puppeteer`/`playwright` won't work in Workers.
## wrangler.json
```json
{
"name": "browser-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER"
}
}
```
**Required:** `nodejs_compat` flag and `browser.binding`.
## TypeScript
```typescript
interface Env {
MYBROWSER: Fetcher;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// ...
}
} satisfies ExportedHandler<Env>;
```
## Development
```bash
wrangler dev --remote # --remote required for browser binding
```
**Local mode does NOT support Browser Rendering** - must use `--remote`.
## REST API
No wrangler config needed. Get API token with "Browser Rendering - Edit" permission.
```bash
curl -X POST \
'https://api.cloudflare.com/client/v4/accounts/{accountId}/browser-rendering/screenshot' \
-H 'Authorization: Bearer TOKEN' \
-d '{"url": "https://example.com"}' --output screenshot.png
```
## Requirements
| Requirement | Value |
|-------------|-------|
| Node.js compatibility | `nodejs_compat` flag |
| Compatibility date | 2023-03-01+ |
| Module format | ES modules only |
| Browser | Chromium 119+ (no Firefox/Safari) |
**Not supported:** WebGL, WebRTC, extensions, `file://` protocol, Service Worker syntax.
## Troubleshooting
| Error | Solution |
|-------|----------|
| `MYBROWSER is undefined` | Use `wrangler dev --remote` |
| `nodejs_compat not enabled` | Add to `compatibility_flags` |
| `Module not found` | `npm install @cloudflare/puppeteer` |
| `Browser Rendering not available` | Enable in dashboard |

View File

@@ -0,0 +1,88 @@
# Browser Rendering Gotchas
## Tier Limits
| Limit | Free | Paid |
|-------|------|------|
| Daily browser time | 10 min | Unlimited* |
| Concurrent sessions | 3 | 30 |
| Requests/minute | 6 | 180 |
| Session keep-alive | 10 min max | 10 min max |
*Subject to fair-use policy.
**Check quota:**
```typescript
const limits = await puppeteer.limits(env.MYBROWSER);
// { remaining: 540000, total: 600000, concurrent: 2 }
```
## Always Close Browsers
```typescript
const browser = await puppeteer.launch(env.MYBROWSER);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
return new Response(await page.content());
} finally {
await browser.close(); // ALWAYS in finally
}
```
**Workers vs REST:** REST auto-closes after timeout. Workers must call `close()` or session stays open until `keep_alive` expires.
## Optimize Concurrency
```typescript
// ❌ 3 sessions (hits free tier limit)
const browser1 = await puppeteer.launch(env.MYBROWSER);
const browser2 = await puppeteer.launch(env.MYBROWSER);
// ✅ 1 session, multiple pages
const browser = await puppeteer.launch(env.MYBROWSER);
const page1 = await browser.newPage();
const page2 = await browser.newPage();
```
## Common Errors
| Error | Cause | Fix |
|-------|-------|-----|
| Session limit exceeded | Too many concurrent | Close unused browsers, use pages not browsers |
| Page navigation timeout | Slow page or `networkidle` on busy page | Increase timeout, use `waitUntil: "load"` |
| Session not found | Expired session | Catch error, launch new session |
| Evaluation failed | DOM element missing | Use `?.` optional chaining |
| Protocol error: Target closed | Page closed during operation | Await all ops before closing |
## page.evaluate() Gotchas
```typescript
// ❌ Outer scope not available
const selector = "h1";
await page.evaluate(() => document.querySelector(selector));
// ✅ Pass as argument
await page.evaluate((sel) => document.querySelector(sel)?.textContent, selector);
```
## Performance
**waitUntil options (fastest to slowest):**
1. `domcontentloaded` - DOM ready
2. `load` - load event (default)
3. `networkidle0` - no network for 500ms
**Block unnecessary resources:**
```typescript
await page.setRequestInterception(true);
page.on("request", (req) => {
if (["image", "stylesheet", "font"].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
```
**Session reuse:** Cold start ~1-2s, warm connect ~100-200ms. Store sessionId in KV for reuse.

View File

@@ -0,0 +1,91 @@
# Browser Rendering Patterns
## Basic Worker
```typescript
import puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const browser = await puppeteer.launch(env.MYBROWSER);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
return new Response(await page.content());
} finally {
await browser.close(); // ALWAYS in finally
}
}
};
```
## Session Reuse
Keep sessions alive for performance:
```typescript
let sessionId = await env.SESSION_KV.get("browser-session");
if (sessionId) {
browser = await puppeteer.connect(env.MYBROWSER, sessionId);
} else {
browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 600000 });
await env.SESSION_KV.put("browser-session", browser.sessionId(), { expirationTtl: 600 });
}
// Don't close browser to keep session alive
```
## Common Operations
| Task | Code |
|------|------|
| Screenshot | `await page.screenshot({ type: "png", fullPage: true })` |
| PDF | `await page.pdf({ format: "A4", printBackground: true })` |
| Extract data | `await page.evaluate(() => document.querySelector('h1').textContent)` |
| Fill form | `await page.type('#input', 'value'); await page.click('button')` |
| Wait nav | `await Promise.all([page.waitForNavigation(), page.click('a')])` |
## Parallel Scraping
```typescript
const pages = await Promise.all(urls.map(() => browser.newPage()));
await Promise.all(pages.map((p, i) => p.goto(urls[i])));
const titles = await Promise.all(pages.map(p => p.title()));
```
## Playwright Selectors
```typescript
import { launch } from "@cloudflare/playwright";
const browser = await launch(env.MYBROWSER);
await page.getByRole("button", { name: "Sign in" }).click();
await page.getByLabel("Email").fill("user@example.com");
await page.getByTestId("submit-button").click();
```
## Incognito Contexts
Isolated sessions without multiple browsers:
```typescript
const ctx1 = await browser.createIncognitoBrowserContext();
const ctx2 = await browser.createIncognitoBrowserContext();
// Each has isolated cookies/storage
```
## Quota Check
```typescript
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.remaining < 60000) return new Response("Quota low", { status: 429 });
```
## Error Handling
```typescript
try {
await page.goto(url, { timeout: 30000, waitUntil: "networkidle0" });
} catch (e) {
if (e.message.includes("timeout")) return new Response("Timeout", { status: 504 });
if (e.message.includes("Session limit")) return new Response("Too many sessions", { status: 429 });
} finally {
if (browser) await browser.close();
}
```

View File

@@ -0,0 +1,111 @@
# C3 (create-cloudflare)
Official CLI for scaffolding Cloudflare Workers and Pages projects with templates, TypeScript, and instant deployment.
## Quick Start
```bash
# Interactive (recommended for first-time)
npm create cloudflare@latest my-app
# Worker (API/WebSocket/Cron)
npm create cloudflare@latest my-api -- --type=hello-world --ts
# Pages (static/SSG/full-stack)
npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
```
## Platform Decision Tree
```
What are you building?
├─ API / WebSocket / Cron / Email handler
│ └─ Workers (default) - no --platform flag needed
│ npm create cloudflare@latest my-api -- --type=hello-world
├─ Static site / SSG / Documentation
│ └─ Pages - requires --platform=pages
│ npm create cloudflare@latest my-site -- --type=web-app --framework=astro --platform=pages
├─ Full-stack app (Next.js/Remix/SvelteKit)
│ ├─ Need Durable Objects, Queues, or Workers-only features?
│ │ └─ Workers (default)
│ └─ Otherwise use Pages for git integration and branch previews
│ └─ Add --platform=pages
└─ Convert existing project
└─ npm create cloudflare@latest . -- --type=pre-existing --existing-script=./src/worker.ts
```
**Critical:** Pages projects require `--platform=pages` flag. Without it, C3 defaults to Workers.
## Interactive Flow
When run without flags, C3 prompts in this order:
1. **Project name** - Directory to create (defaults to current dir with `.`)
2. **Application type** - `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template`
3. **Platform** - `workers` (default) or `pages` (for web apps only)
4. **Framework** - If web-app: `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, etc.
5. **TypeScript** - `yes` (recommended) or `no`
6. **Git** - Initialize repository? `yes` or `no`
7. **Deploy** - Deploy now? `yes` or `no` (requires `wrangler login`)
## Installation Methods
```bash
# NPM
npm create cloudflare@latest
# Yarn
yarn create cloudflare
# PNPM
pnpm create cloudflare@latest
```
## In This Reference
| File | Purpose | Use When |
|------|---------|----------|
| **api.md** | Complete CLI flag reference | Scripting, CI/CD, advanced usage |
| **configuration.md** | Generated files, bindings, types | Understanding output, customization |
| **patterns.md** | Workflows, CI/CD, monorepos | Real-world integration |
| **gotchas.md** | Troubleshooting failures | Deployment blocked, errors |
## Reading Order
| Task | Read |
|------|------|
| Create first project | README only |
| Set up CI/CD | README → api → patterns |
| Debug failed deploy | gotchas |
| Understand generated files | configuration |
| Full CLI reference | api |
| Create custom template | patterns → configuration |
| Convert existing project | README → patterns |
## Post-Creation
```bash
cd my-app
# Local dev with hot reload
npm run dev
# Generate TypeScript types for bindings
npm run cf-typegen
# Deploy to Cloudflare
npm run deploy
```
## See Also
- **workers/README.md** - Workers runtime, bindings, APIs
- **workers-ai/README.md** - AI/ML models
- **pages/README.md** - Pages-specific features
- **wrangler/README.md** - Wrangler CLI beyond initial setup
- **d1/README.md** - SQLite database
- **r2/README.md** - Object storage

View File

@@ -0,0 +1,71 @@
# C3 CLI Reference
## Invocation
```bash
npm create cloudflare@latest [name] [-- flags] # NPM requires --
yarn create cloudflare [name] [flags]
pnpm create cloudflare@latest [name] [-- flags]
```
## Core Flags
| Flag | Values | Description |
|------|--------|-------------|
| `--type` | `hello-world`, `web-app`, `demo`, `pre-existing`, `remote-template` | Application type |
| `--platform` | `workers` (default), `pages` | Target platform |
| `--framework` | `next`, `remix`, `astro`, `react-router`, `solid`, `svelte`, `qwik`, `vue`, `angular`, `hono` | Web framework (requires `--type=web-app`) |
| `--lang` | `ts`, `js`, `python` | Language (for `--type=hello-world`) |
| `--ts` / `--no-ts` | - | TypeScript for web apps |
## Deployment Flags
| Flag | Description |
|------|-------------|
| `--deploy` / `--no-deploy` | Deploy immediately (prompts interactive, skips in CI) |
| `--git` / `--no-git` | Initialize git (default: yes) |
| `--open` | Open browser after deploy |
## Advanced Flags
| Flag | Description |
|------|-------------|
| `--template=user/repo` | GitHub template or local path |
| `--existing-script=./src/worker.ts` | Existing script (requires `--type=pre-existing`) |
| `--category=ai\|database\|realtime` | Demo filter (requires `--type=demo`) |
| `--experimental` | Enable experimental features |
| `--wrangler-defaults` | Skip wrangler prompts |
## Environment Variables
```bash
CLOUDFLARE_API_TOKEN=xxx # For deployment
CLOUDFLARE_ACCOUNT_ID=xxx # Account ID
CF_TELEMETRY_DISABLED=1 # Disable telemetry
```
## Exit Codes
`0` success, `1` user abort, `2` error
## Examples
```bash
# TypeScript Worker
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --no-deploy
# Next.js on Pages
npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts
# Astro blog
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --ts --deploy
# CI: non-interactive
npm create cloudflare@latest my-app -- --type=web-app --framework=next --ts --no-git --no-deploy
# GitHub template
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
# Convert existing project
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./build/worker.js
```

View File

@@ -0,0 +1,81 @@
# C3 Generated Configuration
## Output Structure
```
my-app/
├── src/index.ts # Worker entry point
├── wrangler.jsonc # Cloudflare config
├── package.json # Scripts
├── tsconfig.json
└── .gitignore
```
## wrangler.jsonc
```jsonc
{
"$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-01-27"
}
```
## Binding Placeholders
C3 generates **placeholder IDs** that must be replaced before deploy:
```jsonc
{
"kv_namespaces": [{ "binding": "MY_KV", "id": "placeholder_kv_id" }],
"d1_databases": [{ "binding": "DB", "database_id": "00000000-..." }]
}
```
**Replace with real IDs:**
```bash
npx wrangler kv namespace create MY_KV # Returns real ID
npx wrangler d1 create my-database # Returns real database_id
```
**Deployment error if not replaced:**
```
Error: Invalid KV namespace ID "placeholder_kv_id"
```
## Scripts
```json
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types"
}
}
```
## Type Generation
Run after adding bindings:
```bash
npm run cf-typegen
```
Generates `.wrangler/types/runtime.d.ts`:
```typescript
interface Env {
MY_KV: KVNamespace;
DB: D1Database;
}
```
## Post-Creation Checklist
1. Review `wrangler.jsonc` - check name, compatibility_date
2. Replace placeholder binding IDs with real resource IDs
3. Run `npm run cf-typegen`
4. Test: `npm run dev`
5. Deploy: `npm run deploy`
6. Add secrets: `npx wrangler secret put SECRET_NAME`

View File

@@ -0,0 +1,92 @@
# C3 Troubleshooting
## Deployment Issues
### Placeholder IDs
**Error:** "Invalid namespace ID"
**Fix:** Replace placeholders in wrangler.jsonc with real IDs:
```bash
npx wrangler kv namespace create MY_KV # Get real ID
```
### Authentication
**Error:** "Not authenticated"
**Fix:** `npx wrangler login` or set `CLOUDFLARE_API_TOKEN`
### Name Conflict
**Error:** "Worker already exists"
**Fix:** Change `name` in wrangler.jsonc
## Platform Selection
| Need | Platform |
|------|----------|
| Git integration, branch previews | `--platform=pages` |
| Durable Objects, D1, Queues | Workers (default) |
Wrong platform? Recreate with correct `--platform` flag.
## TypeScript Issues
**"Cannot find name 'KVNamespace'"**
```bash
npm run cf-typegen # Regenerate types
# Restart TS server in editor
```
**Missing types after config change:** Re-run `npm run cf-typegen`
## Package Manager
**Multiple lockfiles causing issues:**
```bash
rm pnpm-lock.yaml # If using npm
rm package-lock.json # If using pnpm
```
## CI/CD
**CI hangs on prompts:**
```bash
npm create cloudflare@latest my-app -- \
--type=hello-world --lang=ts --no-git --no-deploy
```
**Auth in CI:**
```yaml
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
```
## Framework-Specific
| Framework | Issue | Fix |
|-----------|-------|-----|
| Next.js | create-next-app failed | `npm cache clean --force`, retry |
| Astro | Adapter missing | Install `@astrojs/cloudflare` |
| Remix | Module errors | Update `@remix-run/cloudflare*` |
## Compatibility Date
**"Feature X requires compatibility_date >= ..."**
**Fix:** Update `compatibility_date` in wrangler.jsonc to today's date
## Node.js Version
**"Node.js version not supported"**
**Fix:** Install Node.js 18+ (`nvm install 20`)
## Quick Reference
| Error | Cause | Fix |
|-------|-------|-----|
| Invalid namespace ID | Placeholder binding | Create resource, update config |
| Not authenticated | No login | `npx wrangler login` |
| Cannot find KVNamespace | Missing types | `npm run cf-typegen` |
| Worker already exists | Name conflict | Change `name` |
| CI hangs | Missing flags | Add --type, --lang, --no-deploy |
| Template not found | Bad name | Check cloudflare/templates |

View File

@@ -0,0 +1,82 @@
# C3 Usage Patterns
## Quick Workflows
```bash
# TypeScript API Worker
npm create cloudflare@latest my-api -- --type=hello-world --lang=ts --deploy
# Next.js on Pages
npm create cloudflare@latest my-app -- --type=web-app --framework=next --platform=pages --ts --deploy
# Astro static site
npm create cloudflare@latest my-blog -- --type=web-app --framework=astro --platform=pages --ts
```
## CI/CD (GitHub Actions)
```yaml
- name: Deploy
run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
```
**Non-interactive requires:**
```bash
--type=<value> # Required
--no-git # Recommended (CI already in git)
--no-deploy # Deploy separately with secrets
--framework=<value> # For web-app
--ts / --no-ts # Required
```
## Monorepo
C3 detects workspace config (`package.json` workspaces or `pnpm-workspace.yaml`).
```bash
cd packages/
npm create cloudflare@latest my-worker -- --type=hello-world --lang=ts --no-deploy
```
## Custom Templates
```bash
# GitHub repo
npm create cloudflare@latest -- --template=username/repo
npm create cloudflare@latest -- --template=cloudflare/templates/worker-openapi
# Local path
npm create cloudflare@latest my-app -- --template=../my-template
```
**Template requires `c3.config.json`:**
```json
{
"name": "my-template",
"category": "hello-world",
"copies": [{ "path": "src/" }, { "path": "wrangler.jsonc" }],
"transforms": [{ "path": "package.json", "jsonc": { "name": "{{projectName}}" }}]
}
```
## Existing Projects
```bash
# Add Cloudflare to existing Worker
npm create cloudflare@latest . -- --type=pre-existing --existing-script=./dist/index.js
# Add to existing framework app
npm create cloudflare@latest . -- --type=web-app --framework=next --platform=pages --ts
```
## Post-Creation Checklist
1. Review `wrangler.jsonc` - set `compatibility_date`, verify `name`
2. Create bindings: `wrangler kv namespace create`, `wrangler d1 create`, `wrangler r2 bucket create`
3. Generate types: `npm run cf-typegen`
4. Test: `npm run dev`
5. Deploy: `npm run deploy`
6. Set secrets: `wrangler secret put SECRET_NAME`

View File

@@ -0,0 +1,147 @@
# Cloudflare Cache Reserve
**Persistent cache storage built on R2 for long-term content retention**
## Smart Shield Integration
Cache Reserve is part of **Smart Shield**, Cloudflare's comprehensive security and performance suite:
- **Smart Shield Advanced tier**: Includes 2TB Cache Reserve storage
- **Standalone purchase**: Available separately if not using Smart Shield
- **Migration**: Existing standalone customers can migrate to Smart Shield bundles
**Decision**: Already on Smart Shield Advanced? Cache Reserve is included. Otherwise evaluate standalone purchase vs Smart Shield upgrade.
## Overview
Cache Reserve is Cloudflare's persistent, large-scale cache storage layer built on R2. It acts as the ultimate upper-tier cache, storing cacheable content for extended periods (30+ days) to maximize cache hits, reduce origin egress fees, and shield origins from repeated requests for long-tail content.
## Core Concepts
### What is Cache Reserve?
- **Persistent storage layer**: Built on R2, sits above tiered cache hierarchy
- **Long-term retention**: 30-day default retention, extended on each access
- **Automatic operation**: Works seamlessly with existing CDN, no code changes required
- **Origin shielding**: Dramatically reduces origin egress by serving cached content longer
- **Usage-based pricing**: Pay only for storage + read/write operations
### Cache Hierarchy
```
Visitor Request
Lower-Tier Cache (closest to visitor)
↓ (on miss)
Upper-Tier Cache (closest to origin)
↓ (on miss)
Cache Reserve (R2 persistent storage)
↓ (on miss)
Origin Server
```
### How It Works
1. **On cache miss**: Content fetched from origin <20><> written to Cache Reserve + edge caches simultaneously
2. **On edge eviction**: Content may be evicted from edge cache but remains in Cache Reserve
3. **On subsequent request**: If edge cache misses but Cache Reserve hits → content restored to edge caches
4. **Retention**: Assets remain in Cache Reserve for 30 days since last access (configurable via TTL)
## When to Use Cache Reserve
```
Need persistent caching?
├─ High origin egress costs → Cache Reserve ✓
├─ Long-tail content (archives, media libraries) → Cache Reserve ✓
├─ Already using Smart Shield Advanced → Included! ✓
├─ Video streaming with seeking (range requests) → ✗ Not supported
├─ Dynamic/personalized content → ✗ Use edge cache only
├─ Need per-request cache control from Workers → ✗ Use R2 directly
└─ Frequently updated content (< 10hr lifetime) → ✗ Not eligible
```
## Asset Eligibility
Cache Reserve only stores assets meeting **ALL** criteria:
- Cacheable per Cloudflare's standard rules
- Minimum 10-hour TTL (36000 seconds)
- `Content-Length` header present
- Original files only (not transformed images)
### Eligibility Checklist
Use this checklist to verify if an asset is eligible:
- [ ] Zone has Cache Reserve enabled
- [ ] Zone has Tiered Cache enabled (required)
- [ ] Asset TTL ≥ 10 hours (36,000 seconds)
- [ ] `Content-Length` header present on origin response
- [ ] No `Set-Cookie` header (or uses private directive)
- [ ] `Vary` header is NOT `*` (can be `Accept-Encoding`)
- [ ] Not an image transformation variant (original images OK)
- [ ] Not a range request (no HTTP 206 support)
- [ ] Not O2O (Orange-to-Orange) proxied request
**All boxes must be checked for Cache Reserve eligibility.**
### Not Eligible
- Assets with TTL < 10 hours
- Responses without `Content-Length` header
- Image transformation variants (original images are eligible)
- Responses with `Set-Cookie` headers
- Responses with `Vary: *` header
- Assets from R2 public buckets on same zone
- O2O (Orange-to-Orange) setup requests
- **Range requests** (video seeking, partial content downloads)
## Quick Start
```bash
# Enable via Dashboard
https://dash.cloudflare.com/caching/cache-reserve
# Click "Enable Storage Sync" or "Purchase" button
```
**Prerequisites:**
- Paid Cache Reserve plan or Smart Shield Advanced required
- Tiered Cache required for optimal performance
## Essential Commands
```bash
# Check Cache Reserve status
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
-H "Authorization: Bearer $API_TOKEN"
# Enable Cache Reserve
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": "on"}'
# Check asset cache status
curl -I https://example.com/asset.jpg | grep -i cache
```
## In This Reference
| Task | Files |
|------|-------|
| Evaluate if Cache Reserve fits your use case | README.md (this file) |
| Enable Cache Reserve for your zone | README.md + [configuration.md](./configuration.md) |
| Use with Workers (understand limitations) | [api.md](./api.md) |
| Setup via SDKs or IaC (TypeScript, Python, Terraform) | [configuration.md](./configuration.md) |
| Optimize costs and debug issues | [patterns.md](./patterns.md) + [gotchas.md](./gotchas.md) |
| Understand eligibility and troubleshoot | [gotchas.md](./gotchas.md) → [patterns.md](./patterns.md) |
**Files:**
- [configuration.md](./configuration.md) - Setup, API, SDKs, and Cache Rules
- [api.md](./api.md) - Purging, monitoring, Workers integration
- [patterns.md](./patterns.md) - Best practices, cost optimization, debugging
- [gotchas.md](./gotchas.md) - Common issues, limitations, troubleshooting
## See Also
- [r2](../r2/) - Cache Reserve built on R2 storage
- [workers](../workers/) - Workers integration with Cache API

View File

@@ -0,0 +1,194 @@
# Cache Reserve API
## Workers Integration
```
┌────────────────────────────────────────────────────────────────┐
│ CRITICAL: Workers Cache API ≠ Cache Reserve │
│ │
│ • Workers caches.default / cache.put() → edge cache ONLY │
│ • Cache Reserve → zone-level setting, automatic, no per-req │
│ • You CANNOT selectively write to Cache Reserve from Workers │
│ • Cache Reserve works with standard fetch(), not cache.put() │
└────────────────────────────────────────────────────────────────┘
```
Cache Reserve is a **zone-level configuration**, not a per-request API. It works automatically when enabled for the zone:
### Standard Fetch (Recommended)
```typescript
// Cache Reserve works automatically via standard fetch
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Standard fetch uses Cache Reserve automatically
return await fetch(request);
}
};
```
### Cache API Limitations
**IMPORTANT**: `cache.put()` is **NOT compatible** with Cache Reserve or Tiered Cache.
```typescript
// ❌ WRONG: cache.put() bypasses Cache Reserve
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
await cache.put(request, response.clone()); // Bypasses Cache Reserve!
}
// ✅ CORRECT: Use standard fetch for Cache Reserve compatibility
return await fetch(request);
// ✅ CORRECT: Use Cache API only for custom cache namespaces
const customCache = await caches.open('my-custom-cache');
let response = await customCache.match(request);
if (!response) {
response = await fetch(request);
await customCache.put(request, response.clone()); // Custom cache OK
}
```
## Purging and Cache Management
### Purge by URL (Instant)
```typescript
// Purge specific URL from Cache Reserve immediately
const purgeCacheReserveByURL = async (
zoneId: string,
apiToken: string,
urls: string[]
) => {
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 })
}
);
return await response.json();
};
// Example usage
await purgeCacheReserveByURL('zone123', 'token456', [
'https://example.com/image.jpg',
'https://example.com/video.mp4'
]);
```
### Purge by Tag/Host/Prefix (Revalidation)
```typescript
// Purge by cache tag - forces revalidation, not immediate removal
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({ tags: ['tag1', 'tag2'] })
}
);
```
**Purge behavior:**
- **By URL**: Immediate removal from Cache Reserve + edge cache
- **By tag/host/prefix**: Revalidation only, assets remain in storage (costs continue)
### Clear All Cache Reserve Data
```typescript
// Requires Cache Reserve OFF first
await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/cache/cache_reserve_clear`,
{ method: 'POST', headers: { 'Authorization': `Bearer ${apiToken}` } }
);
// Check status: GET same endpoint returns { state: "In-progress" | "Completed" }
```
**Process**: Disable Cache Reserve → Call clear endpoint → Wait up to 24hr → Re-enable
## Monitoring and Analytics
### Dashboard Analytics
Navigate to **Caching > Cache Reserve** to view:
- **Egress Savings**: Total bytes served from Cache Reserve vs origin egress cost saved
- **Requests Served**: Cache Reserve hits vs misses breakdown
- **Storage Used**: Current GB stored in Cache Reserve (billed monthly)
- **Operations**: Class A (writes) and Class B (reads) operation counts
- **Cost Tracking**: Estimated monthly costs based on current usage
### Logpush Integration
```typescript
// Logpush field: CacheReserveUsed (boolean) - filter for Cache Reserve hits
// Query Cache Reserve hits in analytics
const logpushQuery = `
SELECT
ClientRequestHost,
COUNT(*) as requests,
SUM(EdgeResponseBytes) as bytes_served,
COUNT(CASE WHEN CacheReserveUsed = true THEN 1 END) as cache_reserve_hits,
COUNT(CASE WHEN CacheReserveUsed = false THEN 1 END) as cache_reserve_misses
FROM http_requests
WHERE Timestamp >= NOW() - INTERVAL '24 hours'
GROUP BY ClientRequestHost
ORDER BY requests DESC
`;
// Filter only Cache Reserve hits
const crHitsQuery = `
SELECT ClientRequestHost, COUNT(*) as requests, SUM(EdgeResponseBytes) as bytes
FROM http_requests
WHERE CacheReserveUsed = true AND Timestamp >= NOW() - INTERVAL '7 days'
GROUP BY ClientRequestHost
ORDER BY bytes DESC
`;
```
### GraphQL Analytics
```graphql
query CacheReserveAnalytics($zoneTag: string, $since: string, $until: string) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequests1dGroups(
filter: { datetime_geq: $since, datetime_leq: $until }
limit: 1000
) {
dimensions { date }
sum {
cachedBytes
cachedRequests
bytes
requests
}
}
}
}
}
```
## Pricing
```typescript
// Storage: $0.015/GB-month | Class A (writes): $4.50/M | Class B (reads): $0.36/M
// Cache miss: 1A + 1B | Cache hit: 1B | Assets >1GB: proportionally more ops
```
## See Also
- [README](./README.md) - Overview and core concepts
- [Configuration](./configuration.md) - Setup and Cache Rules
- [Patterns](./patterns.md) - Best practices and optimization
- [Gotchas](./gotchas.md) - Common issues and troubleshooting

View File

@@ -0,0 +1,169 @@
# Cache Reserve Configuration
## Dashboard Setup
**Minimum steps to enable:**
```bash
# Navigate to dashboard
https://dash.cloudflare.com/caching/cache-reserve
# Click "Enable Storage Sync" or "Purchase" button
```
**Prerequisites:**
- Paid Cache Reserve plan or Smart Shield Advanced required
- Tiered Cache **required** for Cache Reserve to function optimally
## API Configuration
### REST API
```bash
# Enable
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
-d '{"value": "on"}'
# Check status
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/cache/cache_reserve" \
-H "Authorization: Bearer $API_TOKEN"
```
### TypeScript SDK
```bash
npm install cloudflare
```
```typescript
import Cloudflare from 'cloudflare';
const client = new Cloudflare({
apiToken: process.env.CLOUDFLARE_API_TOKEN,
});
// Enable Cache Reserve
await client.cache.cacheReserve.edit({
zone_id: 'abc123',
value: 'on',
});
// Get Cache Reserve status
const status = await client.cache.cacheReserve.get({
zone_id: 'abc123',
});
console.log(status.value); // 'on' or 'off'
```
### Python SDK
```bash
pip install cloudflare
```
```python
from cloudflare import Cloudflare
client = Cloudflare(api_token=os.environ.get("CLOUDFLARE_API_TOKEN"))
# Enable Cache Reserve
client.cache.cache_reserve.edit(
zone_id="abc123",
value="on"
)
# Get Cache Reserve status
status = client.cache.cache_reserve.get(zone_id="abc123")
print(status.value) # 'on' or 'off'
```
### Terraform
```hcl
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
}
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
resource "cloudflare_zone_cache_reserve" "example" {
zone_id = var.zone_id
enabled = true
}
# Tiered Cache is required for Cache Reserve
resource "cloudflare_tiered_cache" "example" {
zone_id = var.zone_id
cache_type = "smart"
}
```
### Pulumi
```typescript
import * as cloudflare from "@pulumi/cloudflare";
// Enable Cache Reserve
const cacheReserve = new cloudflare.ZoneCacheReserve("example", {
zoneId: zoneId,
enabled: true,
});
// Enable Tiered Cache (required)
const tieredCache = new cloudflare.TieredCache("example", {
zoneId: zoneId,
cacheType: "smart",
});
```
### Required API Token Permissions
- `Zone Settings Read`
- `Zone Settings Write`
- `Zone Read`
- `Zone Write`
## Cache Rules Integration
Control Cache Reserve eligibility via Cache Rules:
```typescript
// Enable for static assets
{
action: 'set_cache_settings',
action_parameters: {
cache_reserve: { eligible: true, minimum_file_ttl: 86400 },
edge_ttl: { mode: 'override_origin', default: 86400 },
cache: true
},
expression: '(http.request.uri.path matches "\\.(jpg|png|webp|pdf|zip)$")'
}
// Disable for APIs
{
action: 'set_cache_settings',
action_parameters: { cache_reserve: { eligible: false } },
expression: '(http.request.uri.path matches "^/api/")'
}
// Create via API: PUT to zones/{zone_id}/rulesets/phases/http_request_cache_settings/entrypoint
```
## Wrangler Integration
Cache Reserve works automatically with Workers deployed via Wrangler. No special wrangler.jsonc configuration needed - enable Cache Reserve via Dashboard or API for the zone.
## See Also
- [README](./README.md) - Overview and core concepts
- [API Reference](./api.md) - Purging and monitoring APIs
- [Patterns](./patterns.md) - Best practices and optimization
- [Gotchas](./gotchas.md) - Common issues and troubleshooting

View File

@@ -0,0 +1,132 @@
# Cache Reserve Gotchas
## Common Errors
### "Assets Not Being Cached in Cache Reserve"
**Cause:** Asset is not cacheable, TTL < 10 hours, Content-Length header missing, or blocking headers present (Set-Cookie, Vary: *)
**Solution:** Ensure minimum TTL of 10+ hours (`Cache-Control: public, max-age=36000`), add Content-Length header, remove Set-Cookie header, and set `Vary: Accept-Encoding` (not *)
### "Range Requests Not Working" (Video Seeking Fails)
**Cause:** Cache Reserve does **NOT** support range requests (HTTP 206 Partial Content)
**Solution:** Range requests bypass Cache Reserve entirely. For video streaming with seeking:
- Use edge cache only (shorter TTLs)
- Consider R2 with direct access for range-heavy workloads
- Accept that seekable content won't benefit from Cache Reserve persistence
### "Origin Bandwidth Higher Than Expected"
**Cause:** Cache Reserve fetches **uncompressed** content from origin, even though it serves compressed to visitors
**Solution:**
- If origin charges by bandwidth, factor in uncompressed transfer costs
- Cache Reserve compresses for visitors automatically (saves visitor bandwidth)
- Compare: origin egress savings vs higher uncompressed fetch costs
### "Cloudflare Images Not Caching with Cache Reserve"
**Cause:** Cloudflare Images with `Vary: Accept` header (format negotiation) is incompatible with Cache Reserve
**Solution:**
- Cache Reserve silently skips images with Vary for format negotiation
- Original images (non-transformed) may still be eligible
- Use Cloudflare Images variants or edge cache for transformed images
### "High Class A Operations Costs"
**Cause:** Frequent cache misses, short TTLs, or frequent revalidation
**Solution:** Increase TTL for stable content (24+ hours), enable Tiered Cache to reduce direct Cache Reserve misses, or use stale-while-revalidate
### "Purge Not Working as Expected"
**Cause:** Purge by tag only triggers revalidation but doesn't remove from Cache Reserve storage
**Solution:** Use purge by URL for immediate removal, or disable Cache Reserve then clear all data for complete removal
### "O2O (Orange-to-Orange) Assets Not Caching"
**Cause:** Orange-to-Orange (proxied zone requesting another proxied zone on Cloudflare) bypasses Cache Reserve
**Solution:**
- **What is O2O**: Zone A (proxied) → Zone B (proxied), both on Cloudflare
- **Detection**: Check `cf-cache-status` for `BYPASS` and review request path
- **Workaround**: Use R2 or direct origin access instead of O2O proxy chains
### "Cache Reserve must be OFF before clearing data"
**Cause:** Attempting to clear Cache Reserve data while it's still enabled
**Solution:** Disable Cache Reserve first, wait briefly for propagation (5s), then clear data (can take up to 24 hours)
## Limits
| Limit | Value | Notes |
|-------|-------|-------|
| Minimum TTL | 10 hours (36000 seconds) | Assets with shorter TTL not eligible |
| Default retention | 30 days (2592000 seconds) | Configurable |
| Maximum file size | Same as R2 limits | No practical limit |
| Purge/clear time | Up to 24 hours | Complete propagation time |
| Plan requirement | Paid Cache Reserve or Smart Shield | Not available on free plans |
| Content-Length header | Required | Must be present for eligibility |
| Set-Cookie header | Blocks caching | Must not be present (or use private directive) |
| Vary header | Cannot be * | Can use Vary: Accept-Encoding |
| Image transformations | Variants not eligible | Original images only |
| Range requests | NOT supported | HTTP 206 bypasses Cache Reserve |
| Compression | Fetches uncompressed | Serves compressed to visitors |
| Worker control | Zone-level only | Cannot control per-request |
| O2O requests | Bypassed | Orange-to-Orange not eligible |
## Additional Resources
- **Official Docs**: https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/
- **API Reference**: https://developers.cloudflare.com/api/resources/cache/subresources/cache_reserve/
- **Cache Rules**: https://developers.cloudflare.com/cache/how-to/cache-rules/
- **Workers Cache API**: https://developers.cloudflare.com/workers/runtime-apis/cache/
- **R2 Documentation**: https://developers.cloudflare.com/r2/
- **Smart Shield**: https://developers.cloudflare.com/smart-shield/
- **Tiered Cache**: https://developers.cloudflare.com/cache/how-to/tiered-cache/
## Troubleshooting Flowchart
Asset not caching in Cache Reserve?
```
1. Is Cache Reserve enabled for zone?
→ No: Enable via Dashboard or API
→ Yes: Continue to step 2
2. Is Tiered Cache enabled?
→ No: Enable Tiered Cache (required!)
→ Yes: Continue to step 3
3. Does asset have TTL ≥ 10 hours?
→ No: Increase via Cache Rules (edge_ttl override)
→ Yes: Continue to step 4
4. Is Content-Length header present?
→ No: Fix origin to include Content-Length
→ Yes: Continue to step 5
5. Is Set-Cookie header present?
→ Yes: Remove Set-Cookie or scope appropriately
→ No: Continue to step 6
6. Is Vary header set to *?
→ Yes: Change to specific value (e.g., Accept-Encoding)
→ No: Continue to step 7
7. Is this a range request?
→ Yes: Range requests bypass Cache Reserve (not supported)
→ No: Continue to step 8
8. Is this an O2O (Orange-to-Orange) request?
→ Yes: O2O bypasses Cache Reserve
→ No: Continue to step 9
9. Check Logpush CacheReserveUsed field
→ Filter logs to see if assets ever hit Cache Reserve
→ Verify cf-cache-status header (should be HIT after first request)
```
## See Also
- [README](./README.md) - Overview and core concepts
- [Configuration](./configuration.md) - Setup and Cache Rules
- [API Reference](./api.md) - Purging and monitoring
- [Patterns](./patterns.md) - Best practices and optimization

View File

@@ -0,0 +1,197 @@
# Cache Reserve Patterns
## Best Practices
### 1. Always Enable Tiered Cache
```typescript
// Cache Reserve is designed for use WITH Tiered Cache
const configuration = {
tieredCache: 'enabled', // Required for optimal performance
cacheReserve: 'enabled', // Works best with Tiered Cache
hierarchy: [
'Lower-Tier Cache (visitor)',
'Upper-Tier Cache (origin region)',
'Cache Reserve (persistent)',
'Origin'
]
};
```
### 2. Set Appropriate Cache-Control Headers
```typescript
// Origin response headers for Cache Reserve eligibility
const originHeaders = {
'Cache-Control': 'public, max-age=86400', // 24hr (minimum 10hr)
'Content-Length': '1024000', // Required
'Cache-Tag': 'images,product-123', // Optional: purging
'ETag': '"abc123"', // Optional: revalidation
// Avoid: 'Set-Cookie' and 'Vary: *' prevent caching
};
```
### 3. Use Cache Rules for Fine-Grained Control
```typescript
// Different TTLs for different content types
const cacheRules = [
{
description: 'Long-term cache for immutable assets',
expression: '(http.request.uri.path matches "^/static/.*\\.[a-f0-9]{8}\\.")',
action_parameters: {
cache_reserve: { eligible: true },
edge_ttl: { mode: 'override_origin', default: 2592000 }, // 30 days
cache: true
}
},
{
description: 'Moderate cache for regular images',
expression: '(http.request.uri.path matches "\\.(jpg|png|webp)$")',
action_parameters: {
cache_reserve: { eligible: true },
edge_ttl: { mode: 'override_origin', default: 86400 }, // 24 hours
cache: true
}
},
{
description: 'Exclude API from Cache Reserve',
expression: '(http.request.uri.path matches "^/api/")',
action_parameters: { cache_reserve: { eligible: false }, cache: false }
}
];
```
### 4. Making Assets Cache Reserve Eligible from Workers
**Note**: This modifies response headers to meet eligibility criteria but does NOT directly control Cache Reserve storage (which is zone-level automatic).
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch(request);
if (!response.ok) return response;
const headers = new Headers(response.headers);
headers.set('Cache-Control', 'public, max-age=36000'); // 10hr minimum
headers.delete('Set-Cookie'); // Blocks caching
// Ensure Content-Length present
if (!headers.has('Content-Length')) {
const blob = await response.blob();
headers.set('Content-Length', blob.size.toString());
return new Response(blob, { status: response.status, headers });
}
return new Response(response.body, { status: response.status, headers });
}
};
```
### 5. Hostname Best Practices
Use Worker's hostname for efficient caching - avoid overriding hostname unnecessarily.
## Architecture Patterns
### Multi-Tier Caching + Immutable Assets
```typescript
// Optimal: L1 (visitor) → L2 (region) → L3 (Cache Reserve) → Origin
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const isImmutable = /\.[a-f0-9]{8,}\.(js|css|jpg|png|woff2)$/.test(url.pathname);
const response = await fetch(request);
if (isImmutable) {
const headers = new Headers(response.headers);
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
return new Response(response.body, { status: response.status, headers });
}
return response;
}
};
```
## Cost Optimization
### Cost Calculator
```typescript
interface CacheReserveEstimate {
avgAssetSizeGB: number;
uniqueAssets: number;
monthlyReads: number;
monthlyWrites: number;
originEgressCostPerGB: number; // e.g., AWS: $0.09/GB
}
function estimateMonthlyCost(input: CacheReserveEstimate) {
// Cache Reserve pricing
const storageCostPerGBMonth = 0.015;
const classAPerMillion = 4.50; // writes
const classBPerMillion = 0.36; // reads
// Calculate Cache Reserve costs
const totalStorageGB = input.avgAssetSizeGB * input.uniqueAssets;
const storageCost = totalStorageGB * storageCostPerGBMonth;
const writeCost = (input.monthlyWrites / 1_000_000) * classAPerMillion;
const readCost = (input.monthlyReads / 1_000_000) * classBPerMillion;
const cacheReserveCost = storageCost + writeCost + readCost;
// Calculate origin egress cost (what you'd pay without Cache Reserve)
const totalTrafficGB = (input.monthlyReads * input.avgAssetSizeGB);
const originEgressCost = totalTrafficGB * input.originEgressCostPerGB;
// Savings calculation
const savings = originEgressCost - cacheReserveCost;
const savingsPercent = ((savings / originEgressCost) * 100).toFixed(1);
return {
cacheReserveCost: `$${cacheReserveCost.toFixed(2)}`,
originEgressCost: `$${originEgressCost.toFixed(2)}`,
monthlySavings: `$${savings.toFixed(2)}`,
savingsPercent: `${savingsPercent}%`,
breakdown: {
storage: `$${storageCost.toFixed(2)}`,
writes: `$${writeCost.toFixed(2)}`,
reads: `$${readCost.toFixed(2)}`,
}
};
}
// Example: Media library
const mediaLibrary = estimateMonthlyCost({
avgAssetSizeGB: 0.005, // 5MB images
uniqueAssets: 10_000,
monthlyReads: 5_000_000,
monthlyWrites: 50_000,
originEgressCostPerGB: 0.09, // AWS S3
});
console.log(mediaLibrary);
// {
// cacheReserveCost: "$9.98",
// originEgressCost: "$25.00",
// monthlySavings: "$15.02",
// savingsPercent: "60.1%",
// breakdown: { storage: "$0.75", writes: "$0.23", reads: "$9.00" }
// }
```
### Optimization Guidelines
- **Set appropriate TTLs**: 10hr minimum, 24hr+ optimal for stable content, 30d max cautiously
- **Cache high-value stable assets**: Images, media, fonts, archives, documentation
- **Exclude frequently changing**: APIs, user-specific content, real-time data
- **Compression note**: Cache Reserve fetches uncompressed from origin, serves compressed to visitors - factor in origin egress costs
## See Also
- [README](./README.md) - Overview and core concepts
- [Configuration](./configuration.md) - Setup and Cache Rules
- [API Reference](./api.md) - Purging and monitoring
- [Gotchas](./gotchas.md) - Common issues and troubleshooting

View File

@@ -0,0 +1,85 @@
# Cloudflare Containers Skill Reference
**APPLIES TO: Cloudflare Containers ONLY - NOT general Cloudflare Workers**
Use when working with Cloudflare Containers: deploying containerized apps on Workers platform, configuring container-enabled Durable Objects, managing container lifecycle, or implementing stateful/stateless container patterns.
## Beta Status
⚠️ Containers is currently in **beta**. API may change without notice. No SLA guarantees. Custom instance types added Jan 2026.
## Core Concepts
**Container as Durable Object:** Each container is a Durable Object with persistent identity. Accessed via `getByName(id)` or `getRandom()`.
**Image deployment:** Images pre-fetched globally. Deployments use rolling strategy (not instant like Workers).
**Lifecycle:** cold start (2-3s) → running → `sleepAfter` timeout → stopped. No autoscaling - manual load balancing via `getRandom()`.
**Persistent identity, ephemeral disk:** Container ID persists, but disk resets on stop. Use Durable Object storage for persistence.
## Quick Start
```typescript
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
defaultPort = 8080;
sleepAfter = "30m";
}
export default {
async fetch(request: Request, env: Env) {
const container = env.MY_CONTAINER.getByName("instance-1");
await container.startAndWaitForPorts();
return container.fetch(request);
}
};
```
## Reading Order
| Task | Files |
|------|-------|
| Setup new container project | README → configuration.md |
| Implement container logic | README → api.md → patterns.md |
| Choose routing pattern | patterns.md (routing section) |
| Debug issues | gotchas.md |
| Production hardening | gotchas.md → patterns.md (lifecycle) |
## Routing Decision Tree
**How should requests reach containers?**
- **Same user/session → same container:** Use `getByName(sessionId)` for session affinity
- **Stateless, spread load:** Use `getRandom()` for load balancing
- **Job per container:** Use `getByName(jobId)` + explicit lifecycle management
- **Single global instance:** Use `getByName("singleton")`
## When to Use Containers vs Workers
**Use Containers when:**
- Need stateful, long-lived processes (sessions, WebSockets, games)
- Running existing containerized apps (Node.js, Python, custom binaries)
- Need filesystem access or specific system dependencies
- Per-user/session isolation with dedicated compute
**Use Workers when:**
- Stateless HTTP handlers
- Sub-millisecond cold starts required
- Auto-scaling to zero critical
- Simple request/response patterns
## In This Reference
- **[configuration.md](configuration.md)** - Wrangler config, instance types, Container class properties, environment variables, account limits
- **[api.md](api.md)** - Container class API, startup methods, communication (HTTP/TCP/WebSocket), routing helpers, lifecycle hooks, scheduling, state inspection
- **[patterns.md](patterns.md)** - Routing patterns (session affinity, load balancing, singleton), WebSocket forwarding, graceful shutdown, Workflow/Queue integration
- **[gotchas.md](gotchas.md)** - Critical gotchas (WebSocket, startup methods), common errors with solutions, specific limits, beta caveats
## See Also
- [Durable Objects](../durable-objects/) - Containers extend Durable Objects
- [Workflows](../workflows/) - Orchestrate container operations
- [Queues](../queues/) - Trigger containers from queue messages
- [Cloudflare Docs](https://developers.cloudflare.com/containers/)

View File

@@ -0,0 +1,187 @@
## Container Class API
```typescript
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
defaultPort = 8080;
requiredPorts = [8080];
sleepAfter = "30m";
enableInternet = true;
pingEndpoint = "/health";
envVars = {};
entrypoint = [];
onStart() { /* container started */ }
onStop() { /* container stopping */ }
onError(error: Error) { /* container error */ }
onActivityExpired(): boolean { /* timeout, return true to stay alive */ }
async alarm() { /* scheduled task */ }
}
```
## Routing
**getByName(id)** - Named instance for session affinity, per-user state
**getRandom()** - Random instance for load balancing stateless services
```typescript
const container = env.MY_CONTAINER.getByName("user-123");
const container = env.MY_CONTAINER.getRandom();
```
## Startup Methods
### start() - Basic start (8s timeout)
```typescript
await container.start();
await container.start({ envVars: { KEY: "value" } });
```
Returns when **process starts**, NOT when ports ready. Use for fire-and-forget.
### startAndWaitForPorts() - Recommended (20s timeout)
```typescript
await container.startAndWaitForPorts(); // Uses requiredPorts
await container.startAndWaitForPorts({ ports: [8080, 9090] });
await container.startAndWaitForPorts({
ports: [8080],
startOptions: { envVars: { KEY: "value" } }
});
```
Returns when **ports listening**. Use before HTTP/TCP requests.
**Port resolution:** explicit ports → requiredPorts → defaultPort → port 33
### waitForPort() - Wait for specific port
```typescript
await container.waitForPort(8080);
await container.waitForPort(8080, { timeout: 30000 });
```
## Communication
### fetch() - HTTP with WebSocket support
```typescript
// ✅ Supports WebSocket upgrades
const response = await container.fetch(request);
const response = await container.fetch("http://container/api", {
method: "POST",
body: JSON.stringify({ data: "value" })
});
```
**Use for:** All HTTP, especially WebSocket.
### containerFetch() - HTTP only (no WebSocket)
```typescript
// ❌ No WebSocket support
const response = await container.containerFetch(request);
```
**⚠️ Critical:** Use `fetch()` for WebSocket, not `containerFetch()`.
### TCP Connections
```typescript
const port = this.ctx.container.getTcpPort(8080);
const conn = port.connect();
await conn.opened;
if (request.body) await request.body.pipeTo(conn.writable);
return new Response(conn.readable);
```
### switchPort() - Change default port
```typescript
this.switchPort(8081); // Subsequent fetch() uses this port
```
## Lifecycle Hooks
### onStart()
Called when container process starts (ports may not be ready). Runs in `blockConcurrencyWhile` - no concurrent requests.
```typescript
onStart() {
console.log("Container starting");
}
```
### onStop()
Called when SIGTERM received. 15 minutes until SIGKILL. Use for graceful shutdown.
```typescript
onStop() {
// Save state, close connections, flush logs
}
```
### onError()
Called when container crashes or fails to start.
```typescript
onError(error: Error) {
console.error("Container error:", error);
}
```
### onActivityExpired()
Called when `sleepAfter` timeout reached. Return `true` to stay alive, `false` to stop.
```typescript
onActivityExpired(): boolean {
if (this.hasActiveConnections()) return true; // Keep alive
return false; // OK to stop
}
```
## Scheduling
```typescript
export class ScheduledContainer extends Container {
async fetch(request: Request) {
await this.schedule(Date.now() + 60000); // 1 minute
await this.schedule("2026-01-28T00:00:00Z"); // ISO string
return new Response("Scheduled");
}
async alarm() {
// Called when schedule fires (SQLite-backed, survives restarts)
}
}
```
**⚠️ Don't override `alarm()` directly when using `schedule()` helper.**
## State Inspection
### External state check
```typescript
const state = await container.getState();
// state.status: "starting" | "running" | "stopping" | "stopped"
```
### Internal state check
```typescript
export class MyContainer extends Container {
async fetch(request: Request) {
if (this.ctx.container.running) { ... }
}
}
```
**⚠️ Use `getState()` for external checks, `ctx.container.running` for internal.**

View File

@@ -0,0 +1,188 @@
## Wrangler Configuration
### Basic Container Config
```jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2026-01-10",
"containers": [
{
"class_name": "MyContainer",
"image": "./Dockerfile", // Path to Dockerfile or directory with Dockerfile
"instance_type": "standard-1", // Predefined or custom (see below)
"max_instances": 10
}
],
"durable_objects": {
"bindings": [
{
"name": "MY_CONTAINER",
"class_name": "MyContainer"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyContainer"] // Must use new_sqlite_classes
}
]
}
```
Key config requirements:
- `image` - Path to Dockerfile or directory containing Dockerfile
- `class_name` - Must match Container class export name
- `max_instances` - Max concurrent container instances
- Must configure Durable Objects binding AND migrations
### Instance Types
#### Predefined Types
| Type | vCPU | Memory | Disk |
|------|------|--------|------|
| lite | 1/16 | 256 MiB | 2 GB |
| basic | 1/4 | 1 GiB | 4 GB |
| standard-1 | 1/2 | 4 GiB | 8 GB |
| standard-2 | 1 | 6 GiB | 12 GB |
| standard-3 | 2 | 8 GiB | 16 GB |
| standard-4 | 4 | 12 GiB | 20 GB |
```jsonc
{
"containers": [
{
"class_name": "MyContainer",
"image": "./Dockerfile",
"instance_type": "standard-2" // Use predefined type
}
]
}
```
#### Custom Types (Jan 2026 Feature)
```jsonc
{
"containers": [
{
"class_name": "MyContainer",
"image": "./Dockerfile",
"instance_type_custom": {
"vcpu": 2, // 1-4 vCPU
"memory_mib": 8192, // 512-12288 MiB (up to 12 GiB)
"disk_mib": 16384 // 2048-20480 MiB (up to 20 GB)
}
}
]
}
```
**Custom type constraints:**
- Minimum 3 GiB memory per vCPU
- Maximum 2 GB disk per 1 GiB memory
- Max 4 vCPU, 12 GiB memory, 20 GB disk per container
### Account Limits
| Resource | Limit | Notes |
|----------|-------|-------|
| Total memory (all containers) | 400 GiB | Across all running containers |
| Total vCPU (all containers) | 100 | Across all running containers |
| Total disk (all containers) | 2 TB | Across all running containers |
| Image storage per account | 50 GB | Stored container images |
### Container Class Properties
```typescript
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container {
// Port Configuration
defaultPort = 8080; // Default port for fetch() calls
requiredPorts = [8080, 9090]; // Ports to wait for in startAndWaitForPorts()
// Lifecycle
sleepAfter = "30m"; // Inactivity timeout (5m, 30m, 2h, etc.)
// Network
enableInternet = true; // Allow outbound internet access
// Health Check
pingEndpoint = "/health"; // Health check endpoint path
// Environment
envVars = { // Environment variables passed to container
NODE_ENV: "production",
LOG_LEVEL: "info"
};
// Startup
entrypoint = ["/bin/start.sh"]; // Override image entrypoint (optional)
}
```
**Property details:**
- **`defaultPort`**: Port used when calling `container.fetch()` without explicit port. Falls back to port 33 if not set.
- **`requiredPorts`**: Array of ports that must be listening before `startAndWaitForPorts()` returns. First port becomes default if `defaultPort` not set.
- **`sleepAfter`**: Duration string (e.g., "5m", "30m", "2h"). Container stops after this period of inactivity. Timer resets on each request.
- **`enableInternet`**: Boolean. If `true`, container can make outbound HTTP/TCP requests.
- **`pingEndpoint`**: Path used for health checks. Should respond with 2xx status.
- **`envVars`**: Object of environment variables. Merged with runtime-provided vars (see below).
- **`entrypoint`**: Array of strings. Overrides container image's CMD/ENTRYPOINT.
### Runtime Environment Variables
Cloudflare automatically provides these environment variables to containers:
| Variable | Description |
|----------|-------------|
| `CLOUDFLARE_APPLICATION_ID` | Worker application ID |
| `CLOUDFLARE_COUNTRY_A2` | Two-letter country code of request origin |
| `CLOUDFLARE_LOCATION` | Cloudflare data center location |
| `CLOUDFLARE_REGION` | Region identifier |
| `CLOUDFLARE_DURABLE_OBJECT_ID` | Container's Durable Object ID |
Custom `envVars` from Container class are merged with these. Custom vars override runtime vars if names conflict.
### Image Management
**Distribution model:** Images pre-fetched to all global locations before deployment. Ensures fast cold starts (2-3s typical).
**Rolling deploys:** Unlike Workers (instant), container deployments roll out gradually. Old versions continue running during rollout.
**Ephemeral disk:** Container disk is ephemeral and resets on each stop. Use Durable Object storage (`this.ctx.storage`) for persistence.
## wrangler.toml Format
```toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2026-01-10"
[[containers]]
class_name = "MyContainer"
image = "./Dockerfile"
instance_type = "standard-2"
max_instances = 10
[[durable_objects.bindings]]
name = "MY_CONTAINER"
class_name = "MyContainer"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["MyContainer"]
```
Both `wrangler.jsonc` and `wrangler.toml` are supported. Use `wrangler.jsonc` for comments and better IDE support.

View File

@@ -0,0 +1,178 @@
## Critical Gotchas
### ⚠️ WebSocket: fetch() vs containerFetch()
**Problem:** WebSocket connections fail silently
**Cause:** `containerFetch()` doesn't support WebSocket upgrades
**Fix:** Always use `fetch()` for WebSocket
```typescript
// ❌ WRONG
return container.containerFetch(request);
// ✅ CORRECT
return container.fetch(request);
```
### ⚠️ startAndWaitForPorts() vs start()
**Problem:** "connection refused" after `start()`
**Cause:** `start()` returns when process starts, NOT when ports ready
**Fix:** Use `startAndWaitForPorts()` before requests
```typescript
// ❌ WRONG
await container.start();
return container.fetch(request);
// ✅ CORRECT
await container.startAndWaitForPorts();
return container.fetch(request);
```
### ⚠️ Activity Timeout on Long Operations
**Problem:** Container stops during long work
**Cause:** `sleepAfter` based on request activity, not internal work
**Fix:** Renew timeout by touching storage
```typescript
const interval = setInterval(() => {
this.ctx.storage.put("keepalive", Date.now());
}, 60000);
try {
await this.doLongWork(data);
} finally {
clearInterval(interval);
}
```
### ⚠️ blockConcurrencyWhile for Startup
**Problem:** Race conditions during initialization
**Fix:** Use `blockConcurrencyWhile` for atomic initialization
```typescript
await this.ctx.blockConcurrencyWhile(async () => {
if (!this.initialized) {
await this.startAndWaitForPorts();
this.initialized = true;
}
});
```
### ⚠️ Lifecycle Hooks Block Requests
**Problem:** Container unresponsive during `onStart()`
**Cause:** Hooks run in `blockConcurrencyWhile` - no concurrent requests
**Fix:** Keep hooks fast, avoid long operations
### ⚠️ Don't Override alarm() When Using schedule()
**Problem:** Scheduled tasks don't execute
**Cause:** `schedule()` uses `alarm()` internally
**Fix:** Implement `alarm()` to handle scheduled tasks
## Common Errors
### "Container start timeout"
**Cause:** Container took >8s (`start()`) or >20s (`startAndWaitForPorts()`)
**Solutions:**
- Optimize image (smaller base, fewer layers)
- Check `entrypoint` correct
- Verify app listens on correct ports
- Increase timeout if needed
### "Port not available"
**Cause:** Calling `fetch()` before port ready
**Solution:** Use `startAndWaitForPorts()`
### "Container memory exceeded"
**Cause:** Using more memory than instance type allows
**Solutions:**
- Use larger instance type (standard-2, standard-3, standard-4)
- Optimize app memory usage
- Use custom instance type
```jsonc
"instance_type_custom": {
"vcpu": 2,
"memory_mib": 8192
}
```
### "Max instances reached"
**Cause:** All `max_instances` slots in use
**Solutions:**
- Increase `max_instances`
- Implement proper `sleepAfter`
- Use `getRandom()` for distribution
- Check for instance leaks
### "No container instance available"
**Cause:** Account capacity limits reached
**Solutions:**
- Check account limits
- Review instance types across containers
- Contact Cloudflare support
## Limits
| Resource | Limit | Notes |
|----------|-------|-------|
| Cold start | 2-3s | Image pre-fetched globally |
| Graceful shutdown | 15 min | SIGTERM → SIGKILL |
| `start()` timeout | 8s | Process start |
| `startAndWaitForPorts()` timeout | 20s | Port ready |
| Max vCPU per container | 4 | standard-4 or custom |
| Max memory per container | 12 GiB | standard-4 or custom |
| Max disk per container | 20 GB | Ephemeral, resets |
| Account total memory | 400 GiB | All containers |
| Account total vCPU | 100 | All containers |
| Account total disk | 2 TB | All containers |
| Image storage | 50 GB | Per account |
| Disk persistence | None | Use DO storage |
## Best Practices
1. **Use `startAndWaitForPorts()` by default** - Prevents port errors
2. **Set appropriate `sleepAfter`** - Balance resources vs cold starts
3. **Use `fetch()` for WebSocket** - Not `containerFetch()`
4. **Design for restarts** - Ephemeral disk, implement graceful shutdown
5. **Monitor resources** - Stay within account limits
6. **Keep hooks fast** - Run in `blockConcurrencyWhile`
7. **Renew activity for long ops** - Touch storage to prevent timeout
## Beta Caveats
⚠️ Containers in **beta**:
- **API may change** without notice
- **No SLA** guarantees
- **Limited regions** initially
- **No autoscaling** - manual via `getRandom()`
- **Rolling deploys** only (not instant like Workers)
Plan for API changes, test thoroughly before production.

View File

@@ -0,0 +1,202 @@
## Routing Patterns
### Session Affinity (Stateful)
```typescript
export class SessionBackend extends Container {
defaultPort = 3000;
sleepAfter = "30m";
}
export default {
async fetch(request: Request, env: Env) {
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
const container = env.SESSION_BACKEND.getByName(sessionId);
await container.startAndWaitForPorts();
return container.fetch(request);
}
};
```
**Use:** User sessions, WebSocket, stateful games, per-user caching.
### Load Balancing (Stateless)
```typescript
export default {
async fetch(request: Request, env: Env) {
const container = env.STATELESS_API.getRandom();
await container.startAndWaitForPorts();
return container.fetch(request);
}
};
```
**Use:** Stateless HTTP APIs, CPU-intensive work, read-only queries.
### Singleton Pattern
```typescript
export default {
async fetch(request: Request, env: Env) {
const container = env.GLOBAL_SERVICE.getByName("singleton");
await container.startAndWaitForPorts();
return container.fetch(request);
}
};
```
**Use:** Global cache, centralized coordinator, single source of truth.
## WebSocket Forwarding
```typescript
export default {
async fetch(request: Request, env: Env) {
if (request.headers.get("Upgrade") === "websocket") {
const sessionId = request.headers.get("X-Session-ID") || crypto.randomUUID();
const container = env.WS_BACKEND.getByName(sessionId);
await container.startAndWaitForPorts();
// ⚠️ MUST use fetch(), not containerFetch()
return container.fetch(request);
}
return new Response("Not a WebSocket request", { status: 400 });
}
};
```
**⚠️ Critical:** Always use `fetch()` for WebSocket.
## Graceful Shutdown
```typescript
export class GracefulContainer extends Container {
private connections = new Set<WebSocket>();
onStop() {
// SIGTERM received, 15 minutes until SIGKILL
for (const ws of this.connections) {
ws.close(1001, "Server shutting down");
}
this.ctx.storage.put("shutdown-time", Date.now());
}
onActivityExpired(): boolean {
return this.connections.size > 0; // Keep alive if connections
}
}
```
## Concurrent Request Handling
```typescript
export class SafeContainer extends Container {
private initialized = false;
async fetch(request: Request) {
await this.ctx.blockConcurrencyWhile(async () => {
if (!this.initialized) {
await this.startAndWaitForPorts();
this.initialized = true;
}
});
return super.fetch(request);
}
}
```
**Use:** One-time initialization, preventing concurrent startup.
## Activity Timeout Renewal
```typescript
export class LongRunningContainer extends Container {
sleepAfter = "5m";
async processLongJob(data: unknown) {
const interval = setInterval(() => {
this.ctx.storage.put("keepalive", Date.now());
}, 60000);
try {
await this.doLongWork(data);
} finally {
clearInterval(interval);
}
}
}
```
**Use:** Long operations exceeding `sleepAfter`.
## Multiple Port Routing
```typescript
export class MultiPortContainer extends Container {
requiredPorts = [8080, 8081, 9090];
async fetch(request: Request) {
const path = new URL(request.url).pathname;
if (path.startsWith("/grpc")) this.switchPort(8081);
else if (path.startsWith("/metrics")) this.switchPort(9090);
return super.fetch(request);
}
}
```
**Use:** Multi-protocol services (HTTP + gRPC), separate metrics endpoints.
## Workflow Integration
```typescript
import { WorkflowEntrypoint } from "cloudflare:workers";
export class ProcessingWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const container = this.env.PROCESSOR.getByName(event.payload.jobId);
await step.do("start", async () => {
await container.startAndWaitForPorts();
});
const result = await step.do("process", async () => {
return container.fetch("/process", {
method: "POST",
body: JSON.stringify(event.payload.data)
}).then(r => r.json());
});
return result;
}
}
```
**Use:** Orchestrating multi-step container operations, durable execution.
## Queue Consumer Integration
```typescript
export default {
async queue(batch, env) {
for (const msg of batch.messages) {
try {
const container = env.PROCESSOR.getByName(msg.body.jobId);
await container.startAndWaitForPorts();
const response = await container.fetch("/process", {
method: "POST",
body: JSON.stringify(msg.body)
});
response.ok ? msg.ack() : msg.retry();
} catch (err) {
console.error("Queue processing error:", err);
msg.retry();
}
}
}
};
```
**Use:** Asynchronous job processing, batch operations, event-driven execution.

View File

@@ -0,0 +1,99 @@
# Cloudflare Cron Triggers
Schedule Workers execution using cron expressions. Runs on Cloudflare's global network during underutilized periods.
## Key Features
- **UTC-only execution** - All schedules run on UTC time
- **5-field cron syntax** - Quartz scheduler extensions (L, W, #)
- **Global propagation** - 15min deployment delay
- **At-least-once delivery** - Rare duplicate executions possible
- **Workflow integration** - Trigger long-running multi-step tasks
- **Green Compute** - Optional carbon-aware scheduling during low-carbon periods
## Cron Syntax
```
┌─────────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌─────── day of month (1-31)
│ │ │ ┌───── month (1-12, JAN-DEC)
│ │ │ │ ┌─── day of week (1-7, SUN-SAT, 1=Sunday)
* * * * *
```
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
## Common Schedules
```bash
*/5 * * * * # Every 5 minutes
0 * * * * # Hourly
0 2 * * * # Daily 2am UTC (off-peak)
0 9 * * MON-FRI # Weekdays 9am UTC
0 0 1 * * # Monthly 1st midnight UTC
0 9 L * * # Last day of month 9am UTC
0 10 * * MON#2 # 2nd Monday 10am UTC
*/10 9-17 * * MON-FRI # Every 10min, 9am-5pm weekdays
```
## Quick Start
**wrangler.jsonc:**
```jsonc
{
"name": "my-cron-worker",
"triggers": {
"crons": ["*/5 * * * *", "0 2 * * *"]
}
}
```
**Handler:**
```typescript
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext,
): Promise<void> {
console.log("Cron:", controller.cron);
console.log("Time:", new Date(controller.scheduledTime));
ctx.waitUntil(asyncTask(env)); // Non-blocking
},
};
```
**Test locally:**
```bash
npx wrangler dev
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
```
## Limits
- **Free:** 3 triggers/worker, 10ms CPU
- **Paid:** Unlimited triggers, 50ms CPU
- **Propagation:** 15min global deployment
- **Timezone:** UTC only
## Reading Order
**New to cron triggers?** Start here:
1. This README - Overview and quick start
2. [configuration.md](./configuration.md) - Set up your first cron trigger
3. [api.md](./api.md) - Understand the handler API
4. [patterns.md](./patterns.md) - Common use cases and examples
**Troubleshooting?** Jump to [gotchas.md](./gotchas.md)
## In This Reference
- [configuration.md](./configuration.md) - wrangler config, env-specific schedules, Green Compute
- [api.md](./api.md) - ScheduledController, noRetry(), waitUntil, testing patterns
- [patterns.md](./patterns.md) - Use cases, monitoring, queue integration, Durable Objects
- [gotchas.md](./gotchas.md) - Timezone issues, idempotency, security, testing
## See Also
- [workflows](../workflows/) - Alternative for long-running scheduled tasks
- [workers](../workers/) - Worker runtime documentation

View File

@@ -0,0 +1,196 @@
# Cron Triggers API
## Basic Handler
```typescript
export default {
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext): Promise<void> {
console.log("Cron executed:", new Date(controller.scheduledTime));
},
};
```
**JavaScript:** Same signature without types
**Python:** `class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx)`
## ScheduledController
```typescript
interface ScheduledController {
scheduledTime: number; // Unix ms when scheduled to run
cron: string; // Expression that triggered (e.g., "*/5 * * * *")
type: string; // Always "scheduled"
noRetry(): void; // Prevent automatic retry on failure
}
```
**Prevent retry on failure:**
```typescript
export default {
async scheduled(controller, env, ctx) {
try {
await riskyOperation(env);
} catch (error) {
// Don't retry - failure is expected/acceptable
controller.noRetry();
console.error("Operation failed, not retrying:", error);
}
},
};
```
**When to use noRetry():**
- External API failures outside your control (avoid hammering failed services)
- Rate limit errors (retry would fail again immediately)
- Duplicate execution detected (idempotency check failed)
- Non-critical operations where skip is acceptable (analytics, caching)
- Validation errors that won't resolve on retry
## Handler Parameters
**`controller: ScheduledController`**
- Access cron expression and scheduled time
**`env: Env`**
- All bindings: KV, R2, D1, secrets, service bindings
**`ctx: ExecutionContext`**
- `ctx.waitUntil(promise)` - Extend execution for async tasks (logging, cleanup, external APIs)
- First `waitUntil` failure recorded in Cron Events
## Multiple Schedules
```typescript
export default {
async scheduled(controller, env, ctx) {
switch (controller.cron) {
case "*/3 * * * *": ctx.waitUntil(updateRecentData(env)); break;
case "0 * * * *": ctx.waitUntil(processHourlyAggregation(env)); break;
case "0 2 * * *": ctx.waitUntil(performDailyMaintenance(env)); break;
default: console.warn(`Unhandled: ${controller.cron}`);
}
},
};
```
## ctx.waitUntil Usage
```typescript
export default {
async scheduled(controller, env, ctx) {
const data = await fetchCriticalData(); // Critical path
// Non-blocking background tasks
ctx.waitUntil(Promise.all([
logToAnalytics(data),
cleanupOldRecords(env.DB),
notifyWebhook(env.WEBHOOK_URL, data),
]));
},
};
```
## Workflow Integration
```typescript
import { WorkflowEntrypoint } from "cloudflare:workers";
export class DataProcessingWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const data = await step.do("fetch-data", () => fetchLargeDataset());
const processed = await step.do("process-data", () => processDataset(data));
await step.do("store-results", () => storeResults(processed));
}
}
export default {
async scheduled(controller, env, ctx) {
const instance = await env.MY_WORKFLOW.create({
params: { scheduledTime: controller.scheduledTime, cron: controller.cron },
});
console.log(`Started workflow: ${instance.id}`);
},
};
```
## Testing Handler
**Local development (/__scheduled endpoint):**
```bash
# Start dev server
npx wrangler dev
# Trigger any cron
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
# Trigger specific cron with custom time
curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000"
```
**Query parameters:**
- `cron` - Required. URL-encoded cron expression (use `+` for spaces)
- `scheduledTime` - Optional. Unix timestamp in milliseconds (defaults to current time)
**Production security:** The `/__scheduled` endpoint is available in production and can be triggered by anyone. Block it or implement authentication - see [gotchas.md](./gotchas.md#security-concerns)
**Unit testing (Vitest):**
```typescript
// test/scheduled.test.ts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:test";
import worker from "../src/index";
describe("Scheduled Handler", () => {
it("processes scheduled event", async () => {
const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: () => {} };
const ctx = { waitUntil: (p: Promise<any>) => p, passThroughOnException: () => {} };
await worker.scheduled(controller, env, ctx);
expect(await env.MY_KV.get("last_run")).toBeDefined();
});
it("handles multiple crons", async () => {
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
await worker.scheduled({ scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled", noRetry: () => {} }, env, ctx);
expect(await env.MY_KV.get("last_type")).toBe("frequent");
});
});
```
## Error Handling
**Automatic retries:**
- Failed cron executions are retried automatically unless `noRetry()` is called
- Retry happens after a delay (typically minutes)
- Only first `waitUntil()` failure is recorded in Cron Events
**Best practices:**
```typescript
export default {
async scheduled(controller, env, ctx) {
try {
await criticalOperation(env);
} catch (error) {
// Log error details
console.error("Cron failed:", {
cron: controller.cron,
scheduledTime: controller.scheduledTime,
error: error.message,
stack: error.stack,
});
// Decide: retry or skip
if (error.message.includes("rate limit")) {
controller.noRetry(); // Skip retry for rate limits
}
// Otherwise allow automatic retry
throw error;
}
},
};
```
## See Also
- [README.md](./README.md) - Overview
- [patterns.md](./patterns.md) - Use cases, examples
- [gotchas.md](./gotchas.md) - Common errors, testing issues

View File

@@ -0,0 +1,180 @@
# Cron Triggers Configuration
## wrangler.jsonc
```jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-cron-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01", // Use current date for new projects
"triggers": {
"crons": [
"*/5 * * * *", // Every 5 minutes
"0 */2 * * *", // Every 2 hours
"0 9 * * MON-FRI", // Weekdays at 9am UTC
"0 2 1 * *" // Monthly on 1st at 2am UTC
]
}
}
```
## Green Compute (Beta)
Schedule crons during low-carbon periods for carbon-aware execution:
```jsonc
{
"name": "eco-cron-worker",
"triggers": {
"crons": ["0 2 * * *"]
},
"placement": {
"mode": "smart" // Runs during low-carbon periods
}
}
```
**Modes:**
- `"smart"` - Carbon-aware scheduling (may delay up to 24h for optimal window)
- Default (no placement config) - Standard scheduling (no delay)
**How it works:**
- Cloudflare delays execution until grid carbon intensity is lower
- Maximum delay: 24 hours from scheduled time
- Ideal for batch jobs with flexible timing requirements
**Use cases:**
- Nightly data processing and ETL pipelines
- Weekly/monthly report generation
- Database backups and maintenance
- Analytics aggregation
- ML model training
**Not suitable for:**
- Time-sensitive operations (SLA requirements)
- User-facing features requiring immediate execution
- Real-time monitoring and alerting
- Compliance tasks with strict time windows
## Environment-Specific Schedules
```jsonc
{
"name": "my-cron-worker",
"triggers": {
"crons": ["0 */6 * * *"] // Prod: every 6 hours
},
"env": {
"staging": {
"triggers": {
"crons": ["*/15 * * * *"] // Staging: every 15min
}
},
"dev": {
"triggers": {
"crons": ["*/5 * * * *"] // Dev: every 5min
}
}
}
}
```
## Schedule Format
**Structure:** `minute hour day-of-month month day-of-week`
**Special chars:** `*` (any), `,` (list), `-` (range), `/` (step), `L` (last), `W` (weekday), `#` (nth)
## Managing Triggers
**Remove all:** `"triggers": { "crons": [] }`
**Preserve existing:** Omit `"triggers"` field entirely
## Deployment
```bash
# Deploy with config crons
npx wrangler deploy
# Deploy specific environment
npx wrangler deploy --env production
# View deployments
npx wrangler deployments list
```
**⚠️ Changes take up to 15 minutes to propagate globally**
## API Management
**Get triggers:**
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
-H "Authorization: Bearer {api_token}"
```
**Update triggers:**
```bash
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json" \
-d '{"crons": ["*/5 * * * *", "0 2 * * *"]}'
```
**Delete all:**
```bash
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/schedules" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json" \
-d '{"crons": []}'
```
## Combining Multiple Workers
For complex schedules, use multiple workers:
```jsonc
// worker-frequent.jsonc
{
"name": "data-sync-frequent",
"triggers": { "crons": ["*/5 * * * *"] }
}
// worker-daily.jsonc
{
"name": "reports-daily",
"triggers": { "crons": ["0 2 * * *"] },
"placement": { "mode": "smart" }
}
// worker-weekly.jsonc
{
"name": "cleanup-weekly",
"triggers": { "crons": ["0 3 * * SUN"] }
}
```
**Benefits:**
- Separate CPU limits per worker
- Independent error isolation
- Different Green Compute policies
- Easier to maintain and debug
## Validation
**Test cron syntax:**
- [crontab.guru](https://crontab.guru/) - Interactive validator
- Wrangler validates on deploy but won't catch logic errors
**Common mistakes:**
- `0 0 * * *` runs daily at midnight UTC, not your local timezone
- `*/60 * * * *` is invalid (use `0 * * * *` for hourly)
- `0 2 31 * *` only runs on months with 31 days
## See Also
- [README.md](./README.md) - Overview, quick start
- [api.md](./api.md) - Handler implementation
- [patterns.md](./patterns.md) - Multi-cron routing examples

View File

@@ -0,0 +1,199 @@
# Cron Triggers Gotchas
## Common Errors
### "Timezone Issues"
**Problem:** Cron runs at wrong time relative to local timezone
**Cause:** All crons execute in UTC, no local timezone support
**Solution:** Convert local time to UTC manually
**Conversion formula:** `utcHour = (localHour - utcOffset + 24) % 24`
**Examples:**
- 9am PST (UTC-8) → `(9 - (-8) + 24) % 24 = 17``0 17 * * *`
- 2am EST (UTC-5) → `(2 - (-5) + 24) % 24 = 7``0 7 * * *`
- 6pm JST (UTC+9) → `(18 - 9 + 24) % 24 = 33 % 24 = 9``0 9 * * *`
**Daylight Saving Time:** Adjust manually when DST changes, or schedule at times unaffected by DST (e.g., 2am-4am local time usually safe)
### "Cron Not Executing"
**Cause:** Missing `scheduled()` export, invalid syntax, propagation delay (<15min), or plan limits
**Solution:** Verify export exists, validate at crontab.guru, wait 15+ min after deploy, check plan limits
### "Duplicate Executions"
**Cause:** At-least-once delivery
**Solution:** Track execution IDs in KV - see idempotency pattern below
### "Execution Failures"
**Cause:** CPU exceeded, unhandled exceptions, network timeouts, binding errors
**Solution:** Use try-catch, AbortController timeouts, `ctx.waitUntil()` for long ops, or Workflows for heavy tasks
### "Local Testing Not Working"
**Problem:** `/__scheduled` endpoint returns 404 or doesn't trigger handler
**Cause:** Missing `scheduled()` export, wrangler not running, or incorrect endpoint format
**Solution:**
1. Verify `scheduled()` is exported:
```typescript
export default {
async scheduled(controller, env, ctx) {
console.log("Cron triggered");
},
};
```
2. Start dev server:
```bash
npx wrangler dev
```
3. Use correct endpoint format (URL-encode spaces as `+`):
```bash
# Correct
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
# Wrong (will fail)
curl "http://localhost:8787/__scheduled?cron=*/5 * * * *"
```
4. Update Wrangler if outdated:
```bash
npm install -g wrangler@latest
```
### "waitUntil() Tasks Not Completing"
**Problem:** Background tasks in `ctx.waitUntil()` fail silently or don't execute
**Cause:** Promises rejected without error handling, or handler returns before promise settles
**Solution:** Always await or handle errors in waitUntil promises:
```typescript
export default {
async scheduled(controller, env, ctx) {
// BAD: Silent failures
ctx.waitUntil(riskyOperation());
// GOOD: Explicit error handling
ctx.waitUntil(
riskyOperation().catch(err => {
console.error("Background task failed:", err);
return logError(err, env);
})
);
},
};
```
### "Idempotency Issues"
**Problem:** At-least-once delivery causes duplicate side effects (double charges, duplicate emails)
**Cause:** No deduplication mechanism
**Solution:** Use KV to track execution IDs:
```typescript
export default {
async scheduled(controller, env, ctx) {
const executionId = `${controller.cron}-${controller.scheduledTime}`;
const existing = await env.EXECUTIONS.get(executionId);
if (existing) {
console.log("Already executed, skipping");
controller.noRetry();
return;
}
await env.EXECUTIONS.put(executionId, "1", { expirationTtl: 86400 }); // 24h TTL
await performIdempotentOperation(env);
},
};
```
### "Security Concerns"
**Problem:** `__scheduled` endpoint exposed in production allows unauthorized cron triggering
**Cause:** Testing endpoint available in deployed Workers
**Solution:** Block `__scheduled` in production:
```typescript
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Block __scheduled in production
if (url.pathname === "/__scheduled" && env.ENVIRONMENT === "production") {
return new Response("Not Found", { status: 404 });
}
return handleRequest(request, env, ctx);
},
async scheduled(controller, env, ctx) {
// Your cron logic
},
};
```
**Also:** Use `env.API_KEY` for secrets (never hardcode)
**Alternative:** Add middleware to verify request origin:
```typescript
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/__scheduled") {
// Check Cloudflare headers to verify internal request
const cfRay = request.headers.get("cf-ray");
if (!cfRay && env.ENVIRONMENT === "production") {
return new Response("Not Found", { status: 404 });
}
}
return handleRequest(request, env, ctx);
},
async scheduled(controller, env, ctx) {
// Your cron logic
},
};
```
## Limits & Quotas
| Limit | Free | Paid | Notes |
|-------|------|------|-------|
| Triggers per Worker | 3 | Unlimited | Maximum cron schedules per Worker |
| CPU time | 10ms | 50ms | May need `ctx.waitUntil()` or Workflows |
| Execution guarantee | At-least-once | At-least-once | Duplicates possible - use idempotency |
| Propagation delay | Up to 15 minutes | Up to 15 minutes | Time for changes to take effect globally |
| Min interval | 1 minute | 1 minute | Cannot schedule more frequently |
| Cron accuracy | ±1 minute | ±1 minute | Execution may drift slightly |
## Testing Best Practices
**Unit tests:**
- Mock `ScheduledController`, `ExecutionContext`, and bindings
- Test each cron expression separately
- Verify `noRetry()` is called when expected
- Use Vitest with `@cloudflare/vitest-pool-workers` for realistic env
**Integration tests:**
- Test via `/__scheduled` endpoint in dev environment
- Verify idempotency logic with duplicate `scheduledTime` values
- Test error handling and retry behavior
**Production:** Start with long intervals (`*/30 * * * *`), monitor Cron Events for 24h, set up alerts before reducing interval
## Resources
- [Cron Triggers Docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/)
- [Scheduled Handler API](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)
- [Cloudflare Workflows](https://developers.cloudflare.com/workflows/)
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)
- [Crontab Guru](https://crontab.guru/) - Validator
- [Vitest Pool Workers](https://github.com/cloudflare/workers-sdk/tree/main/fixtures/vitest-pool-workers-examples)

View File

@@ -0,0 +1,190 @@
# Cron Triggers Patterns
## API Data Sync
```typescript
export default {
async scheduled(controller, env, ctx) {
const response = await fetch("https://api.example.com/data", {headers: { "Authorization": `Bearer ${env.API_KEY}` }});
if (!response.ok) throw new Error(`API error: ${response.status}`);
ctx.waitUntil(env.MY_KV.put("cached_data", JSON.stringify(await response.json()), {expirationTtl: 3600}));
},
};
```
## Database Cleanup
```typescript
export default {
async scheduled(controller, env, ctx) {
const result = await env.DB.prepare(`DELETE FROM sessions WHERE expires_at < datetime('now')`).run();
console.log(`Deleted ${result.meta.changes} expired sessions`);
ctx.waitUntil(env.DB.prepare("VACUUM").run());
},
};
```
## Report Generation
```typescript
export default {
async scheduled(controller, env, ctx) {
const startOfWeek = new Date(); startOfWeek.setDate(startOfWeek.getDate() - 7);
const { results } = await env.DB.prepare(`SELECT date, revenue, orders FROM daily_stats WHERE date >= ? ORDER BY date`).bind(startOfWeek.toISOString()).all();
const report = {period: "weekly", totalRevenue: results.reduce((sum, d) => sum + d.revenue, 0), totalOrders: results.reduce((sum, d) => sum + d.orders, 0), dailyBreakdown: results};
const reportKey = `reports/weekly-${Date.now()}.json`;
await env.REPORTS_BUCKET.put(reportKey, JSON.stringify(report));
ctx.waitUntil(env.SEND_EMAIL.fetch("https://example.com/send", {method: "POST", body: JSON.stringify({to: "team@example.com", subject: "Weekly Report", reportUrl: `https://reports.example.com/${reportKey}`})}));
},
};
```
## Health Checks
```typescript
export default {
async scheduled(controller, env, ctx) {
const services = [{name: "API", url: "https://api.example.com/health"}, {name: "CDN", url: "https://cdn.example.com/health"}];
const checks = await Promise.all(services.map(async (service) => {
const start = Date.now();
try {
const response = await fetch(service.url, { signal: AbortSignal.timeout(5000) });
return {name: service.name, status: response.ok ? "up" : "down", responseTime: Date.now() - start};
} catch (error) {
return {name: service.name, status: "down", responseTime: Date.now() - start, error: error.message};
}
}));
ctx.waitUntil(env.STATUS_KV.put("health_status", JSON.stringify(checks)));
const failures = checks.filter(c => c.status === "down");
if (failures.length > 0) ctx.waitUntil(fetch(env.ALERT_WEBHOOK, {method: "POST", body: JSON.stringify({text: `${failures.length} service(s) down: ${failures.map(f => f.name).join(", ")}`})}));
},
};
```
## Batch Processing (Rate-Limited)
```typescript
export default {
async scheduled(controller, env, ctx) {
const queueData = await env.QUEUE_KV.get("pending_items", "json");
if (!queueData || queueData.length === 0) return;
const batch = queueData.slice(0, 100);
const results = await Promise.allSettled(batch.map(item => fetch("https://api.example.com/process", {method: "POST", headers: {"Authorization": `Bearer ${env.API_KEY}`, "Content-Type": "application/json"}, body: JSON.stringify(item)})));
console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.length} items`);
ctx.waitUntil(env.QUEUE_KV.put("pending_items", JSON.stringify(queueData.slice(100))));
},
};
```
## Queue Integration
```typescript
export default {
async scheduled(controller, env, ctx) {
const batch = await env.MY_QUEUE.receive({ batchSize: 100 });
const results = await Promise.allSettled(batch.messages.map(async (msg) => {
await processMessage(msg.body, env);
await msg.ack();
}));
console.log(`Processed ${results.filter(r => r.status === "fulfilled").length}/${batch.messages.length}`);
},
};
```
## Monitoring & Observability
```typescript
export default {
async scheduled(controller, env, ctx) {
const startTime = Date.now();
const meta = { cron: controller.cron, scheduledTime: controller.scheduledTime };
console.log("[START]", meta);
try {
const result = await performTask(env);
console.log("[SUCCESS]", { ...meta, duration: Date.now() - startTime, count: result.count });
ctx.waitUntil(env.METRICS.put(`cron:${controller.scheduledTime}`, JSON.stringify({ ...meta, status: "success" }), { expirationTtl: 2592000 }));
} catch (error) {
console.error("[ERROR]", { ...meta, duration: Date.now() - startTime, error: error.message });
ctx.waitUntil(fetch(env.ALERT_WEBHOOK, { method: "POST", body: JSON.stringify({ text: `Cron failed: ${controller.cron}`, error: error.message }) }));
throw error;
}
},
};
```
**View logs:** `npx wrangler tail` or Dashboard → Workers & Pages → Worker → Logs
## Durable Objects Coordination
```typescript
export default {
async scheduled(controller, env, ctx) {
const stub = env.COORDINATOR.get(env.COORDINATOR.idFromName("cron-lock"));
const acquired = await stub.tryAcquireLock(controller.scheduledTime);
if (!acquired) {
controller.noRetry();
return;
}
try {
await performTask(env);
} finally {
await stub.releaseLock();
}
},
};
```
## Python Handler
```python
from workers import WorkerEntrypoint
class Default(WorkerEntrypoint):
async def scheduled(self, controller, env, ctx):
data = await env.MY_KV.get("key")
ctx.waitUntil(env.DB.execute("DELETE FROM logs WHERE created_at < datetime('now', '-7 days')"))
```
## Testing Patterns
**Local testing with /__scheduled:**
```bash
# Start dev server
npx wrangler dev
# Test specific cron
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"
# Test with specific time
curl "http://localhost:8787/__scheduled?cron=0+2+*+*+*&scheduledTime=1704067200000"
```
**Unit tests:**
```typescript
// test/scheduled.test.ts
import { describe, it, expect, vi } from "vitest";
import { env } from "cloudflare:test";
import worker from "../src/index";
describe("Scheduled Handler", () => {
it("executes cron", async () => {
const controller = { scheduledTime: Date.now(), cron: "*/5 * * * *", type: "scheduled" as const, noRetry: vi.fn() };
const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn() };
await worker.scheduled(controller, env, ctx);
expect(await env.MY_KV.get("last_run")).toBeDefined();
});
it("calls noRetry on duplicate", async () => {
const controller = { scheduledTime: 1704067200000, cron: "0 2 * * *", type: "scheduled" as const, noRetry: vi.fn() };
await env.EXECUTIONS.put("0 2 * * *-1704067200000", "1");
await worker.scheduled(controller, env, { waitUntil: vi.fn(), passThroughOnException: vi.fn() });
expect(controller.noRetry).toHaveBeenCalled();
});
});
```
## See Also
- [README.md](./README.md) - Overview
- [api.md](./api.md) - Handler implementation
- [gotchas.md](./gotchas.md) - Troubleshooting

View File

@@ -0,0 +1,133 @@
# Cloudflare D1 Database
Expert guidance for Cloudflare D1, a serverless SQLite database designed for horizontal scale-out across multiple databases.
## Overview
D1 is Cloudflare's managed, serverless database with:
- SQLite SQL semantics and compatibility
- Built-in disaster recovery via Time Travel (30-day point-in-time recovery)
- Horizontal scale-out architecture (10 GB per database)
- Worker and HTTP API access
- Pricing based on query and storage costs only
**Architecture Philosophy**: D1 is optimized for per-user, per-tenant, or per-entity database patterns rather than single large databases.
## Quick Start
```bash
# Create database
wrangler d1 create <database-name>
# Execute migration
wrangler d1 migrations apply <db-name> --remote
# Local development
wrangler dev
```
## Core Query Methods
```typescript
// .all() - Returns all rows; .first() - First row or null; .first(col) - Single column value
// .run() - INSERT/UPDATE/DELETE; .raw() - Array of arrays (efficient)
const { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all();
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
```
## Batch Operations
```typescript
// Multiple queries in single round trip (atomic transaction)
const results = await env.DB.batch([
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1),
env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1),
env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1)
]);
```
## Sessions API (Paid Plans)
```typescript
// Create long-running session for analytics/migrations (up to 15 minutes)
const session = env.DB.withSession();
try {
await session.prepare('CREATE INDEX idx_heavy ON large_table(column)').run();
await session.prepare('ANALYZE').run();
} finally {
session.close(); // Always close to release resources
}
```
## Read Replication (Paid Plans)
```typescript
// Read from nearest replica for lower latency (automatic failover)
const user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// Writes always go to primary
await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();
```
## Platform Limits
| Limit | Free Tier | Paid Plans |
|-------|-----------|------------|
| Database size | 500 MB | 10 GB per database |
| Row size | 1 MB max | 1 MB max |
| Query timeout | 30 seconds | 30 seconds |
| Batch size | 1,000 statements | 10,000 statements |
| Time Travel retention | 7 days | 30 days |
| Read replicas | Not available | Yes (paid add-on) |
**Pricing**: $5/month per database beyond free tier + $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month
## CLI Commands
```bash
# Database management
wrangler d1 create <db-name>
wrangler d1 list
wrangler d1 delete <db-name>
# Migrations
wrangler d1 migrations create <db-name> <migration-name> # Create new migration file
wrangler d1 migrations apply <db-name> --remote # Apply pending migrations
wrangler d1 migrations apply <db-name> --local # Apply locally
wrangler d1 migrations list <db-name> --remote # Show applied migrations
# Direct SQL execution
wrangler d1 execute <db-name> --remote --command="SELECT * FROM users"
wrangler d1 execute <db-name> --local --file=./schema.sql
# Backups & Import/Export
wrangler d1 export <db-name> --remote --output=./backup.sql # Full export with schema
wrangler d1 export <db-name> --remote --no-schema --output=./data.sql # Data only
wrangler d1 time-travel restore <db-name> --timestamp="2024-01-15T14:30:00Z" # Point-in-time recovery
# Development
wrangler dev --persist-to=./.wrangler/state
```
## Reading Order
**Start here**: Quick Start above → configuration.md (setup) → api.md (queries)
**Common tasks**:
- First time setup: configuration.md → Run migrations
- Adding queries: api.md → Prepared statements
- Pagination/caching: patterns.md
- Production optimization: Read Replication + Sessions API (this file)
- Debugging: gotchas.md
## In This Reference
- [configuration.md](./configuration.md) - wrangler.jsonc setup, migrations, TypeScript types, ORMs, local dev
- [api.md](./api.md) - Query methods (.all/.first/.run/.raw), batch, sessions, read replicas, error handling
- [patterns.md](./patterns.md) - Pagination, bulk operations, caching, multi-tenant, sessions, analytics
- [gotchas.md](./gotchas.md) - SQL injection, limits by plan tier, performance, common errors
## See Also
- [workers](../workers/) - Worker runtime and fetch handler patterns
- [hyperdrive](../hyperdrive/) - Connection pooling for external databases

View File

@@ -0,0 +1,196 @@
# D1 API Reference
## Prepared Statements (Required for Security)
```typescript
// ❌ NEVER: Direct string interpolation (SQL injection risk)
const result = await env.DB.prepare(`SELECT * FROM users WHERE id = ${userId}`).all();
// ✅ CORRECT: Prepared statements with bind()
const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();
// Multiple parameters
const result = await env.DB.prepare('SELECT * FROM users WHERE email = ? AND active = ?').bind(email, true).all();
```
## Query Execution Methods
```typescript
// .all() - Returns all rows
const { results, success, meta } = await env.DB.prepare('SELECT * FROM users WHERE active = ?').bind(true).all();
// results: Array of row objects; success: boolean
// meta: { duration: number, rows_read: number, rows_written: number }
// .first() - Returns first row or null
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// .first(columnName) - Returns single column value
const email = await env.DB.prepare('SELECT email FROM users WHERE id = ?').bind(userId).first('email');
// Returns string | number | null
// .run() - For INSERT/UPDATE/DELETE (no row data returned)
const result = await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();
// result.meta: { duration, rows_read, rows_written, last_row_id, changes }
// .raw() - Returns array of arrays (efficient for large datasets)
const rawResults = await env.DB.prepare('SELECT id, name FROM users').raw();
// [[1, 'Alice'], [2, 'Bob']]
```
## Batch Operations
```typescript
// Execute multiple queries in single round trip (atomic transaction)
const results = await env.DB.batch([
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(1),
env.DB.prepare('SELECT * FROM posts WHERE author_id = ?').bind(1),
env.DB.prepare('UPDATE users SET last_access = ? WHERE id = ?').bind(Date.now(), 1)
]);
// results is array: [result1, result2, result3]
// Batch with same prepared statement, different params
const userIds = [1, 2, 3];
const stmt = env.DB.prepare('SELECT * FROM users WHERE id = ?');
const results = await env.DB.batch(userIds.map(id => stmt.bind(id)));
```
## Transactions (via batch)
```typescript
// D1 executes batch() as atomic transaction - all succeed or all fail
const results = await env.DB.batch([
env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(1, 100),
env.DB.prepare('INSERT INTO accounts (id, balance) VALUES (?, ?)').bind(2, 200),
env.DB.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(50, 1),
env.DB.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(50, 2)
]);
```
## Sessions API (Paid Plans)
Long-running sessions for operations exceeding 30s timeout (up to 15 min).
```typescript
const session = env.DB.withSession({ timeout: 600 }); // 10 min (1-900s)
try {
await session.prepare('CREATE INDEX idx_large ON big_table(column)').run();
await session.prepare('ANALYZE').run();
} finally {
session.close(); // CRITICAL: always close to prevent leaks
}
```
**Use cases**: Migrations, ANALYZE, large index creation, bulk transformations
## Read Replication (Paid Plans)
Routes queries to nearest replica for lower latency. Writes always go to primary.
```typescript
interface Env {
DB: D1Database; // Primary (writes)
DB_REPLICA: D1Database; // Replica (reads)
}
// Reads: use replica
const user = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
// Writes: use primary
await env.DB.prepare('UPDATE users SET last_login = ? WHERE id = ?').bind(Date.now(), userId).run();
// Read-after-write: use primary for consistency (replication lag <100ms-2s)
await env.DB.prepare('INSERT INTO posts (title) VALUES (?)').bind(title).run();
const post = await env.DB.prepare('SELECT * FROM posts WHERE title = ?').bind(title).first(); // Primary
```
## Error Handling
```typescript
async function getUser(userId: number, env: Env): Promise<Response> {
try {
const result = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();
if (!result.success) return new Response('Database error', { status: 500 });
if (result.results.length === 0) return new Response('User not found', { status: 404 });
return Response.json(result.results[0]);
} catch (error) {
return new Response('Internal error', { status: 500 });
}
}
// Constraint violations
try {
await env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind(email, name).run();
} catch (error) {
if (error.message?.includes('UNIQUE constraint failed')) return new Response('Email exists', { status: 409 });
throw error;
}
```
## REST API (HTTP) Access
Access D1 from external services (non-Worker contexts) using Cloudflare API.
```typescript
// Single query
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sql: 'SELECT * FROM users WHERE id = ?',
params: [userId]
})
}
);
const { result, success, errors } = await response.json();
// result: [{ results: [...], success: true, meta: {...} }]
// Batch queries via HTTP
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/d1/database/${DATABASE_ID}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${CLOUDFLARE_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify([
{ sql: 'SELECT * FROM users WHERE id = ?', params: [1] },
{ sql: 'SELECT * FROM posts WHERE author_id = ?', params: [1] }
])
}
);
```
**Use cases**: Server-side scripts, CI/CD migrations, administrative tools, non-Worker integrations
## Testing & Debugging
```typescript
// Vitest with unstable_dev
import { unstable_dev } from 'wrangler';
describe('D1', () => {
let worker: Awaited<ReturnType<typeof unstable_dev>>;
beforeAll(async () => { worker = await unstable_dev('src/index.ts'); });
afterAll(async () => { await worker.stop(); });
it('queries users', async () => { expect((await worker.fetch('/users')).status).toBe(200); });
});
// Debug query performance
const result = await env.DB.prepare('SELECT * FROM users').all();
console.log('Duration:', result.meta.duration, 'ms');
// Query plan analysis
const plan = await env.DB.prepare('EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?').bind(email).all();
```
```bash
# Inspect local database
sqlite3 .wrangler/state/v3/d1/<database-id>.sqlite
.tables; .schema users; PRAGMA table_info(users);
```

View File

@@ -0,0 +1,188 @@
# D1 Configuration
## wrangler.jsonc Setup
```jsonc
{
"name": "your-worker-name",
"main": "src/index.ts",
"compatibility_date": "2025-01-01", // Use current date for new projects
"d1_databases": [
{
"binding": "DB", // Env variable name
"database_name": "your-db-name", // Human-readable name
"database_id": "your-database-id", // UUID from dashboard/CLI
"migrations_dir": "migrations" // Optional: default is "migrations"
},
// Read replica (paid plans only)
{
"binding": "DB_REPLICA",
"database_name": "your-db-name",
"database_id": "your-database-id" // Same ID, different binding
},
// Multiple databases
{
"binding": "ANALYTICS_DB",
"database_name": "analytics-db",
"database_id": "yyy-yyy-yyy"
}
]
}
```
## TypeScript Types
```typescript
interface Env { DB: D1Database; ANALYTICS_DB?: D1Database; }
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const result = await env.DB.prepare('SELECT * FROM users').all();
return Response.json(result.results);
}
}
```
## Migrations
File structure: `migrations/0001_initial_schema.sql`, `0002_add_posts.sql`, etc.
### Example Migration
```sql
-- migrations/0001_initial_schema.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_published ON posts(published);
```
### Running Migrations
```bash
# Create new migration file
wrangler d1 migrations create <db-name> add_users_table
# Creates: migrations/0001_add_users_table.sql
# Apply migrations
wrangler d1 migrations apply <db-name> --local # Apply to local DB
wrangler d1 migrations apply <db-name> --remote # Apply to production DB
# List applied migrations
wrangler d1 migrations list <db-name> --remote
# Direct SQL execution (bypasses migration tracking)
wrangler d1 execute <db-name> --remote --command="SELECT * FROM users"
wrangler d1 execute <db-name> --local --file=./schema.sql
```
**Migration tracking**: Wrangler creates `d1_migrations` table automatically to track applied migrations
## Indexing Strategy
```sql
-- Index frequently queried columns
CREATE INDEX idx_users_email ON users(email);
-- Composite indexes for multi-column queries
CREATE INDEX idx_posts_user_published ON posts(user_id, published);
-- Covering indexes (include queried columns)
CREATE INDEX idx_users_email_name ON users(email, name);
-- Partial indexes for filtered queries
CREATE INDEX idx_active_users ON users(email) WHERE active = 1;
-- Check if query uses index
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?;
```
## Drizzle ORM
```typescript
// drizzle.config.ts
export default {
schema: './src/schema.ts', out: './migrations', dialect: 'sqlite', driver: 'd1-http',
dbCredentials: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, databaseId: process.env.D1_DATABASE_ID!, token: process.env.CLOUDFLARE_API_TOKEN! }
} satisfies Config;
// schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name').notNull()
});
// worker.ts
import { drizzle } from 'drizzle-orm/d1';
import { users } from './schema';
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB);
return Response.json(await db.select().from(users));
}
}
```
## Import & Export
```bash
# Export full database (schema + data)
wrangler d1 export <db-name> --remote --output=./backup.sql
# Export data only (no schema)
wrangler d1 export <db-name> --remote --no-schema --output=./data-only.sql
# Export with foreign key constraints preserved
# (Default: foreign keys are disabled during export for import compatibility)
# Import SQL file
wrangler d1 execute <db-name> --remote --file=./backup.sql
# Limitations
# - BLOB data may not export correctly (use R2 for binary files)
# - Very large exports (>1GB) may timeout (split into chunks)
# - Import is NOT atomic (use batch() for transactional imports in Workers)
```
## Plan Tiers
| Feature | Free | Paid |
|---------|------|------|
| Database size | 500 MB | 10 GB |
| Batch size | 1,000 statements | 10,000 statements |
| Time Travel | 7 days | 30 days |
| Read replicas | ❌ | ✅ |
| Sessions API | ❌ | ✅ (up to 15 min) |
| Pricing | Free | $5/mo + usage |
**Usage pricing** (paid plans): $0.001 per 1K reads + $1 per 1M writes + $0.75/GB storage/month
## Local Development
```bash
wrangler dev --persist-to=./.wrangler/state # Persist across restarts
# Local DB: .wrangler/state/v3/d1/<database-id>.sqlite
sqlite3 .wrangler/state/v3/d1/<database-id>.sqlite # Inspect
# Local dev uses free tier limits by default
```

View File

@@ -0,0 +1,98 @@
# D1 Gotchas & Troubleshooting
## Common Errors
### "SQL Injection Vulnerability"
**Cause:** Using string interpolation instead of prepared statements with bind()
**Solution:** ALWAYS use prepared statements: `env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all()` instead of string interpolation which allows attackers to inject malicious SQL
### "no such table"
**Cause:** Table doesn't exist because migrations haven't been run, or using wrong database binding
**Solution:** Run migrations using `wrangler d1 migrations apply <db-name> --remote` and verify binding name in wrangler.jsonc matches code
### "UNIQUE constraint failed"
**Cause:** Attempting to insert duplicate value in column with UNIQUE constraint
**Solution:** Catch error and return 409 Conflict status code
### "Query Timeout (30s exceeded)"
**Cause:** Query execution exceeds 30 second timeout limit
**Solution:** Break into smaller queries, add indexes to speed up queries, or reduce dataset size
### "N+1 Query Problem"
**Cause:** Making multiple individual queries in a loop instead of single optimized query
**Solution:** Use JOIN to fetch related data in single query or use `batch()` method for multiple queries
### "Missing Indexes"
**Cause:** Queries performing full table scans without indexes
**Solution:** Use `EXPLAIN QUERY PLAN` to check if index is used, then create index with `CREATE INDEX idx_users_email ON users(email)`
### "Boolean Type Issues"
**Cause:** SQLite uses INTEGER (0/1) not native boolean type
**Solution:** Bind 1 or 0 instead of true/false when working with boolean values
### "Date/Time Type Issues"
**Cause:** SQLite doesn't have native DATE/TIME types
**Solution:** Use TEXT (ISO 8601 format) or INTEGER (unix timestamp) for date/time values
## Plan Tier Limits
| Limit | Free Tier | Paid Plans | Notes |
|-------|-----------|------------|-------|
| Database size | 500 MB | 10 GB | Design for multiple DBs per tenant on paid |
| Row size | 1 MB | 1 MB | Store large files in R2, not D1 |
| Query timeout | 30s | 30s (900s with sessions) | Use sessions API for migrations |
| Batch size | 1,000 statements | 10,000 statements | Split large batches accordingly |
| Time Travel | 7 days | 30 days | Point-in-time recovery window |
| Read replicas | ❌ Not available | ✅ Available | Paid add-on for lower latency |
| Sessions API | ❌ Not available | ✅ Up to 15 min | For migrations and heavy operations |
| Concurrent requests | 10,000/min | Higher | Contact support for custom limits |
## Production Gotchas
### "Batch size exceeded"
**Cause:** Attempting to send >1,000 statements on free tier or >10,000 on paid
**Solution:** Chunk batches: `for (let i = 0; i < stmts.length; i += MAX_BATCH) await env.DB.batch(stmts.slice(i, i + MAX_BATCH))`
### "Session not closed / resource leak"
**Cause:** Forgot to call `session.close()` after using sessions API
**Solution:** Always use try/finally block: `try { await session.prepare(...) } finally { session.close() }`
### "Replication lag causing stale reads"
**Cause:** Reading from replica immediately after write - replication lag can be 100ms-2s
**Solution:** Use primary for read-after-write: `await env.DB.prepare(...)` not `env.DB_REPLICA`
### "Migration applied to local but not remote"
**Cause:** Forgot `--remote` flag when applying migrations
**Solution:** Always run `wrangler d1 migrations apply <db-name> --remote` for production
### "Foreign key constraint failed"
**Cause:** Inserting row with FK to non-existent parent, or deleting parent before children
**Solution:** Enable FK enforcement: `PRAGMA foreign_keys = ON;` and use ON DELETE CASCADE in schema
### "BLOB data corrupted on export"
**Cause:** D1 export may not handle BLOB correctly
**Solution:** Store binary files in R2, only store R2 URLs/keys in D1
### "Database size approaching limit"
**Cause:** Storing too much data in single database
**Solution:** Horizontal scale-out: create per-tenant/per-user databases, archive old data, or upgrade to paid plan
### "Local dev vs production behavior differs"
**Cause:** Local uses SQLite file, production uses distributed D1 - different performance/limits
**Solution:** Always test migrations on remote with `--remote` flag before production rollout

View File

@@ -0,0 +1,189 @@
# D1 Patterns & Best Practices
## Pagination
```typescript
async function getUsers({ page, pageSize }: { page: number; pageSize: number }, env: Env) {
const offset = (page - 1) * pageSize;
const [countResult, dataResult] = await env.DB.batch([
env.DB.prepare('SELECT COUNT(*) as total FROM users'),
env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?').bind(pageSize, offset)
]);
return { data: dataResult.results, total: countResult.results[0].total, page, pageSize, totalPages: Math.ceil(countResult.results[0].total / pageSize) };
}
```
## Conditional Queries
```typescript
async function searchUsers(filters: { name?: string; email?: string; active?: boolean }, env: Env) {
const conditions: string[] = [], params: (string | number | boolean | null)[] = [];
if (filters.name) { conditions.push('name LIKE ?'); params.push(`%${filters.name}%`); }
if (filters.email) { conditions.push('email = ?'); params.push(filters.email); }
if (filters.active !== undefined) { conditions.push('active = ?'); params.push(filters.active ? 1 : 0); }
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
return await env.DB.prepare(`SELECT * FROM users ${whereClause}`).bind(...params).all();
}
```
## Bulk Insert
```typescript
async function bulkInsertUsers(users: Array<{ name: string; email: string }>, env: Env) {
const stmt = env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const batch = users.map(user => stmt.bind(user.name, user.email));
return await env.DB.batch(batch);
}
```
## Caching with KV
```typescript
async function getCachedUser(userId: number, env: { DB: D1Database; CACHE: KVNamespace }) {
const cacheKey = `user:${userId}`;
const cached = await env.CACHE?.get(cacheKey, 'json');
if (cached) return cached;
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
if (user) await env.CACHE?.put(cacheKey, JSON.stringify(user), { expirationTtl: 300 });
return user;
}
```
## Query Optimization
```typescript
// ✅ Use indexes in WHERE clauses
const users = await env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email).all();
// ✅ Limit result sets
const recentPosts = await env.DB.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT 100').all();
// ✅ Use batch() for multiple independent queries
const [user, posts, comments] = await env.DB.batch([
env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId),
env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(userId),
env.DB.prepare('SELECT * FROM comments WHERE user_id = ?').bind(userId)
]);
// ❌ Avoid N+1 queries
for (const post of posts) {
const author = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(post.user_id).first(); // Bad: multiple round trips
}
// ✅ Use JOINs instead
const postsWithAuthors = await env.DB.prepare(`
SELECT posts.*, users.name as author_name
FROM posts
JOIN users ON posts.user_id = users.id
`).all();
```
## Multi-Tenant SaaS
```typescript
// Each tenant gets own database
export default {
async fetch(request: Request, env: { [key: `TENANT_${string}`]: D1Database }) {
const tenantId = request.headers.get('X-Tenant-ID');
const data = await env[`TENANT_${tenantId}`].prepare('SELECT * FROM records').all();
return Response.json(data.results);
}
}
```
## Session Storage
```typescript
async function createSession(userId: number, token: string, env: Env) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
return await env.DB.prepare('INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)').bind(userId, token, expiresAt).run();
}
async function validateSession(token: string, env: Env) {
return await env.DB.prepare('SELECT s.*, u.email FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > CURRENT_TIMESTAMP').bind(token).first();
}
```
## Analytics/Events
```typescript
async function logEvent(event: { type: string; userId?: number; metadata: object }, env: Env) {
return await env.DB.prepare('INSERT INTO events (type, user_id, metadata) VALUES (?, ?, ?)').bind(event.type, event.userId || null, JSON.stringify(event.metadata)).run();
}
async function getEventStats(startDate: string, endDate: string, env: Env) {
return await env.DB.prepare('SELECT type, COUNT(*) as count FROM events WHERE timestamp BETWEEN ? AND ? GROUP BY type ORDER BY count DESC').bind(startDate, endDate).all();
}
```
## Read Replication Pattern (Paid Plans)
```typescript
interface Env { DB: D1Database; DB_REPLICA: D1Database; }
export default {
async fetch(request: Request, env: Env) {
if (request.method === 'GET') {
// Reads: use replica for lower latency
const users = await env.DB_REPLICA.prepare('SELECT * FROM users WHERE active = 1').all();
return Response.json(users.results);
}
if (request.method === 'POST') {
const { name, email } = await request.json();
const result = await env.DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run();
// Read-after-write: use primary for consistency (replication lag <100ms-2s)
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(result.meta.last_row_id).first();
return Response.json(user, { status: 201 });
}
}
}
```
**Use replicas for**: Analytics dashboards, search results, public queries (eventual consistency OK)
**Use primary for**: Read-after-write, financial transactions, authentication (consistency required)
## Sessions API Pattern (Paid Plans)
```typescript
// Migration with long-running session (up to 15 min)
async function runMigration(env: Env) {
const session = env.DB.withSession({ timeout: 600 }); // 10 min
try {
await session.prepare('CREATE INDEX idx_users_email ON users(email)').run();
await session.prepare('CREATE INDEX idx_posts_user ON posts(user_id)').run();
await session.prepare('ANALYZE').run();
} finally {
session.close(); // Always close to prevent leaks
}
}
// Bulk transformation with batching
async function transformLargeDataset(env: Env) {
const session = env.DB.withSession({ timeout: 900 }); // 15 min max
try {
const BATCH_SIZE = 1000;
let offset = 0;
while (true) {
const rows = await session.prepare('SELECT id, data FROM legacy LIMIT ? OFFSET ?').bind(BATCH_SIZE, offset).all();
if (rows.results.length === 0) break;
const updates = rows.results.map(row =>
session.prepare('UPDATE legacy SET new_data = ? WHERE id = ?').bind(transform(row.data), row.id)
);
await session.batch(updates);
offset += BATCH_SIZE;
}
} finally { session.close(); }
}
```
## Time Travel & Backups
```bash
wrangler d1 time-travel restore <db-name> --timestamp="2024-01-15T14:30:00Z" # Point-in-time
wrangler d1 time-travel info <db-name> # List restore points (7 days free, 30 days paid)
wrangler d1 export <db-name> --remote --output=./backup.sql # Full export
wrangler d1 export <db-name> --remote --no-schema --output=./data.sql # Data only
wrangler d1 execute <db-name> --remote --file=./backup.sql # Import
```

View File

@@ -0,0 +1,41 @@
# Cloudflare DDoS Protection
Autonomous, always-on protection against DDoS attacks across L3/4 and L7.
## Protection Types
- **HTTP DDoS (L7)**: Protects HTTP/HTTPS traffic, phase `ddos_l7`, zone/account level
- **Network DDoS (L3/4)**: UDP/SYN/DNS floods, phase `ddos_l4`, account level only
- **Adaptive DDoS**: Learns 7-day baseline, detects deviations, 4 profile types (Origins, User-Agents, Locations, Protocols)
## Plan Availability
| Feature | Free | Pro | Business | Enterprise | Enterprise Advanced |
|---------|------|-----|----------|------------|---------------------|
| HTTP DDoS (L7) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Network DDoS (L3/4) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Override rules | 1 | 1 | 1 | 1 | 10 |
| Custom expressions | ✗ | ✗ | ✗ | ✗ | ✓ |
| Log action | ✗ | ✗ | ✗ | ✗ | ✓ |
| Adaptive DDoS | ✗ | ✗ | ✗ | ✓ | ✓ |
| Alert filters | Basic | Basic | Basic | Advanced | Advanced |
## Actions & Sensitivity
- **Actions**: `block`, `managed_challenge`, `challenge`, `log` (Enterprise Advanced only)
- **Sensitivity**: `default` (high), `medium`, `low`, `eoff` (essentially off)
- **Override**: By category/tag or individual rule ID
- **Scope**: Zone-level overrides take precedence over account-level
## Reading Order
| File | Purpose | Start Here If... |
|------|---------|------------------|
| [configuration.md](./configuration.md) | Dashboard setup, rule structure, adaptive profiles | You're setting up DDoS protection for the first time |
| [api.md](./api.md) | API endpoints, SDK usage, ruleset ID discovery | You're automating configuration or need programmatic access |
| [patterns.md](./patterns.md) | Protection strategies, defense-in-depth, dynamic response | You need implementation patterns or layered security |
| [gotchas.md](./gotchas.md) | False positives, tuning, error handling | You're troubleshooting or optimizing existing protection |
## See Also
- [waf](../waf/) - Application-layer security rules
- [bot-management](../bot-management/) - Bot detection and mitigation

View File

@@ -0,0 +1,164 @@
# DDoS API
## Endpoints
### HTTP DDoS (L7)
```typescript
// Zone-level
PUT /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint
GET /zones/{zoneId}/rulesets/phases/ddos_l7/entrypoint
// Account-level (Enterprise Advanced)
PUT /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint
GET /accounts/{accountId}/rulesets/phases/ddos_l7/entrypoint
```
### Network DDoS (L3/4)
```typescript
// Account-level only
PUT /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint
GET /accounts/{accountId}/rulesets/phases/ddos_l4/entrypoint
```
## TypeScript SDK
**SDK Version**: Requires `cloudflare` >= 3.0.0 for ruleset phase methods.
```typescript
import Cloudflare from "cloudflare";
const client = new Cloudflare({ apiToken: process.env.CLOUDFLARE_API_TOKEN });
// STEP 1: Discover managed ruleset ID (required for overrides)
const allRulesets = await client.rulesets.list({ zone_id: zoneId });
const ddosRuleset = allRulesets.result.find(
(r) => r.kind === "managed" && r.phase === "ddos_l7"
);
if (!ddosRuleset) throw new Error("DDoS managed ruleset not found");
const managedRulesetId = ddosRuleset.id;
// STEP 2: Get current HTTP DDoS configuration
const entrypointRuleset = await client.zones.rulesets.phases.entrypoint.get("ddos_l7", {
zone_id: zoneId,
});
// STEP 3: Update HTTP DDoS ruleset with overrides
await client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
zone_id: zoneId,
rules: [
{
action: "execute",
expression: "true",
action_parameters: {
id: managedRulesetId, // From discovery step
overrides: {
sensitivity_level: "medium",
action: "managed_challenge",
},
},
},
],
});
// Network DDoS (account level, L3/4)
const l4Rulesets = await client.rulesets.list({ account_id: accountId });
const l4DdosRuleset = l4Rulesets.result.find(
(r) => r.kind === "managed" && r.phase === "ddos_l4"
);
const l4Ruleset = await client.accounts.rulesets.phases.entrypoint.get("ddos_l4", {
account_id: accountId,
});
```
## Alert Configuration
```typescript
interface DDoSAlertConfig {
name: string;
enabled: boolean;
alert_type: "http_ddos_attack_alert" | "layer_3_4_ddos_attack_alert"
| "advanced_http_ddos_attack_alert" | "advanced_layer_3_4_ddos_attack_alert";
filters?: {
zones?: string[];
hostnames?: string[];
requests_per_second?: number;
packets_per_second?: number;
megabits_per_second?: number;
ip_prefixes?: string[]; // CIDR
ip_addresses?: string[];
protocols?: string[];
};
mechanisms: {
email?: Array<{ id: string }>;
webhooks?: Array<{ id: string }>;
pagerduty?: Array<{ id: string }>;
};
}
// Create alert
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${accountId}/alerting/v3/policies`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(alertConfig),
}
);
```
## Typed Override Examples
```typescript
// Override by category
interface CategoryOverride {
action: "execute";
expression: string;
action_parameters: {
id: string;
overrides: {
categories?: Array<{
category: "http-flood" | "http-anomaly" | "udp-flood" | "syn-flood";
sensitivity_level?: "default" | "medium" | "low" | "eoff";
action?: "block" | "managed_challenge" | "challenge" | "log";
}>;
};
};
}
// Override by rule ID
interface RuleOverride {
action: "execute";
expression: string;
action_parameters: {
id: string;
overrides: {
rules?: Array<{
id: string;
action?: "block" | "managed_challenge" | "challenge" | "log";
sensitivity_level?: "default" | "medium" | "low" | "eoff";
}>;
};
};
}
// Example: Override specific adaptive rule
const adaptiveOverride: RuleOverride = {
action: "execute",
expression: "true",
action_parameters: {
id: managedRulesetId,
overrides: {
rules: [
{ id: "...adaptive-origins-rule-id...", sensitivity_level: "low" },
],
},
},
};
```
See [patterns.md](./patterns.md) for complete implementation patterns.

View File

@@ -0,0 +1,93 @@
# DDoS Configuration
## Dashboard Setup
1. Navigate to Security > DDoS
2. Select HTTP DDoS or Network-layer DDoS
3. Configure sensitivity & action per ruleset/category/rule
4. Apply overrides with optional expressions (Enterprise Advanced)
5. Enable Adaptive DDoS toggle (Enterprise/Enterprise Advanced, requires 7 days traffic history)
## Rule Structure
```typescript
interface DDoSOverride {
description: string;
rules: Array<{
action: "execute";
expression: string; // Custom expression (Enterprise Advanced) or "true" for all
action_parameters: {
id: string; // Managed ruleset ID (discover via api.md)
overrides: {
sensitivity_level?: "default" | "medium" | "low" | "eoff";
action?: "block" | "managed_challenge" | "challenge" | "log"; // log = Enterprise Advanced only
categories?: Array<{
category: string; // e.g., "http-flood", "udp-flood"
sensitivity_level?: string;
}>;
rules?: Array<{
id: string;
action?: string;
sensitivity_level?: string;
}>;
};
};
}>;
}
```
## Expression Availability
| Plan | Custom Expressions | Example |
|------|-------------------|---------|
| Free/Pro/Business | ✗ | Use `"true"` only |
| Enterprise | ✗ | Use `"true"` only |
| Enterprise Advanced | ✓ | `ip.src in {...}`, `http.request.uri.path matches "..."` |
## Sensitivity Mapping
| UI | API | Threshold |
|----|-----|-----------|
| High | `default` | Most aggressive |
| Medium | `medium` | Balanced |
| Low | `low` | Less aggressive |
| Essentially Off | `eoff` | Minimal mitigation |
## Common Categories
- `http-flood`, `http-anomaly` (L7)
- `udp-flood`, `syn-flood`, `dns-flood` (L3/4)
## Override Precedence
Multiple override layers apply in this order (higher precedence wins):
```
Zone-level > Account-level
Individual Rule > Category > Global sensitivity/action
```
**Example**: Zone rule for `/api/*` overrides account-level global settings.
## Adaptive DDoS Profiles
**Availability**: Enterprise, Enterprise Advanced
**Learning period**: 7 days of traffic history required
| Profile Type | Description | Detects |
|--------------|-------------|---------|
| **Origins** | Traffic patterns per origin server | Anomalous requests to specific origins |
| **User-Agents** | Traffic patterns per User-Agent | Malicious/anomalous user agent strings |
| **Locations** | Traffic patterns per geo-location | Attacks from specific countries/regions |
| **Protocols** | Traffic patterns per protocol (L3/4) | Protocol-specific flood attacks |
Configure by targeting specific adaptive rule IDs via API (see api.md#typed-override-examples).
## Alerting
Configure via Notifications:
- Alert types: `http_ddos_attack_alert`, `layer_3_4_ddos_attack_alert`, `advanced_*` variants
- Filters: zones, hostnames, RPS/PPS/Mbps thresholds, IPs, protocols
- Mechanisms: email, webhooks, PagerDuty
See [api.md](./api.md#alert-configuration) for API examples.

View File

@@ -0,0 +1,107 @@
# DDoS Gotchas
## Common Errors
### "False positives blocking legitimate traffic"
**Cause**: Sensitivity too high, wrong action, or missing exceptions
**Solution**:
1. Lower sensitivity for specific rule/category
2. Use `log` action first to validate (Enterprise Advanced)
3. Add exception with custom expression (e.g., allowlist IPs)
4. Query flagged requests via GraphQL Analytics API to identify patterns
### "Attacks getting through"
**Cause**: Sensitivity too low or wrong action
**Solution**: Increase to `default` sensitivity and use `block` action:
```typescript
const config = {
rules: [{
expression: "true",
action: "execute",
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } },
}],
};
```
### "Adaptive rules not working"
**Cause**: Insufficient traffic history (needs 7 days)
**Solution**: Wait for baseline to establish, check dashboard for adaptive rule status
### "Zone override ignored"
**Cause**: Account overrides conflict with zone overrides
**Solution**: Configure at zone level OR remove zone overrides to use account-level
### "Log action not available"
**Cause**: Not on Enterprise Advanced DDoS plan
**Solution**: Use `managed_challenge` with low sensitivity for testing
### "Rule limit exceeded"
**Cause**: Too many override rules (Free/Pro/Business: 1, Enterprise Advanced: 10)
**Solution**: Combine conditions in single expression using `and`/`or`
### "Cannot override rule"
**Cause**: Rule is read-only
**Solution**: Check API response for read-only indicator, use different rule
### "Cannot disable DDoS protection"
**Cause**: DDoS managed rulesets cannot be fully disabled (always-on protection)
**Solution**: Set `sensitivity_level: "eoff"` for minimal mitigation
### "Expression not allowed"
**Cause**: Custom expressions require Enterprise Advanced plan
**Solution**: Use `expression: "true"` for all traffic, or upgrade plan
### "Managed ruleset not found"
**Cause**: Zone/account doesn't have DDoS managed ruleset, or incorrect phase
**Solution**: Verify ruleset exists via `client.rulesets.list()`, check phase name (`ddos_l7` or `ddos_l4`)
## API Error Codes
| Error Code | Message | Cause | Solution |
|------------|---------|-------|----------|
| 10000 | Authentication error | Invalid/missing API token | Check token has DDoS permissions |
| 81000 | Ruleset validation failed | Invalid rule structure | Verify `action_parameters.id` is managed ruleset ID |
| 81020 | Expression not allowed | Custom expressions on wrong plan | Use `"true"` or upgrade to Enterprise Advanced |
| 81021 | Rule limit exceeded | Too many override rules | Reduce rules or upgrade (Enterprise Advanced: 10) |
| 81022 | Invalid sensitivity level | Wrong sensitivity value | Use: `default`, `medium`, `low`, `eoff` |
| 81023 | Invalid action | Wrong action for plan | Enterprise Advanced only: `log` action |
## Limits
| Resource/Limit | Free/Pro/Business | Enterprise | Enterprise Advanced |
|----------------|-------------------|------------|---------------------|
| Override rules per zone | 1 | 1 | 10 |
| Custom expressions | ✗ | ✗ | ✓ |
| Log action | ✗ | ✗ | ✓ |
| Adaptive DDoS | ✗ | ✓ | ✓ |
| Traffic history required | - | 7 days | 7 days |
## Tuning Strategy
1. Start with `log` action + `medium` sensitivity
2. Monitor for 24-48 hours
3. Identify false positives, add exceptions
4. Gradually increase to `default` sensitivity
5. Change action from `log``managed_challenge``block`
6. Document all adjustments
## Best Practices
- Test during low-traffic periods
- Use zone-level for per-site tuning
- Reference IP lists for easier management
- Set appropriate alert thresholds (avoid noise)
- Combine with WAF for layered defense
- Avoid over-tuning (keep config simple)
See [patterns.md](./patterns.md) for progressive rollout examples.

View File

@@ -0,0 +1,174 @@
# DDoS Protection Patterns
## Allowlist Trusted IPs
```typescript
const config = {
description: "Allowlist trusted IPs",
rules: [{
expression: "ip.src in { 203.0.113.0/24 192.0.2.1 }",
action: "execute",
action_parameters: {
id: managedRulesetId,
overrides: { sensitivity_level: "eoff" },
},
}],
};
await client.accounts.rulesets.phases.entrypoint.update("ddos_l7", {
account_id: accountId,
...config,
});
```
## Route-specific Sensitivity
```typescript
const config = {
description: "Route-specific protection",
rules: [
{
expression: "not http.request.uri.path matches \"^/api/\"",
action: "execute",
action_parameters: {
id: managedRulesetId,
overrides: { sensitivity_level: "default", action: "block" },
},
},
{
expression: "http.request.uri.path matches \"^/api/\"",
action: "execute",
action_parameters: {
id: managedRulesetId,
overrides: { sensitivity_level: "low", action: "managed_challenge" },
},
},
],
};
```
## Progressive Enhancement
```typescript
enum ProtectionLevel { MONITORING = "monitoring", LOW = "low", MEDIUM = "medium", HIGH = "high" }
const levelConfig = {
[ProtectionLevel.MONITORING]: { action: "log", sensitivity: "eoff" },
[ProtectionLevel.LOW]: { action: "managed_challenge", sensitivity: "low" },
[ProtectionLevel.MEDIUM]: { action: "managed_challenge", sensitivity: "medium" },
[ProtectionLevel.HIGH]: { action: "block", sensitivity: "default" },
} as const;
async function setProtectionLevel(zoneId: string, level: ProtectionLevel, rulesetId: string, client: Cloudflare) {
const settings = levelConfig[level];
return client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
zone_id: zoneId,
rules: [{
expression: "true",
action: "execute",
action_parameters: { id: rulesetId, overrides: { action: settings.action, sensitivity_level: settings.sensitivity } },
}],
});
}
```
## Dynamic Response to Attacks
```typescript
interface Env { CLOUDFLARE_API_TOKEN: string; ZONE_ID: string; KV: KVNamespace; }
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.url.includes("/attack-detected")) {
const attackData = await request.json();
await env.KV.put(`attack:${Date.now()}`, JSON.stringify(attackData), { expirationTtl: 86400 });
const recentAttacks = await getRecentAttacks(env.KV);
if (recentAttacks.length > 5) {
await setProtectionLevel(env.ZONE_ID, ProtectionLevel.HIGH, managedRulesetId, client);
return new Response("Protection increased");
}
}
return new Response("OK");
},
async scheduled(event: ScheduledEvent, env: Env): Promise<void> {
const recentAttacks = await getRecentAttacks(env.KV);
if (recentAttacks.length === 0) await setProtectionLevel(env.ZONE_ID, ProtectionLevel.MEDIUM, managedRulesetId, client);
},
};
```
## Multi-rule Tiered Protection (Enterprise Advanced)
```typescript
const config = {
description: "Multi-tier DDoS protection",
rules: [
{
expression: "not ip.src in $known_ips and not cf.bot_management.score gt 30",
action: "execute",
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "default", action: "block" } },
},
{
expression: "cf.bot_management.verified_bot",
action: "execute",
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "medium", action: "managed_challenge" } },
},
{
expression: "ip.src in $trusted_ips",
action: "execute",
action_parameters: { id: managedRulesetId, overrides: { sensitivity_level: "low" } },
},
],
};
```
## Defense in Depth
Layered security stack: DDoS + WAF + Rate Limiting + Bot Management.
```typescript
// Layer 1: DDoS (volumetric attacks)
await client.zones.rulesets.phases.entrypoint.update("ddos_l7", {
zone_id: zoneId,
rules: [{ expression: "true", action: "execute", action_parameters: { id: ddosRulesetId, overrides: { sensitivity_level: "medium" } } }],
});
// Layer 2: WAF (exploit protection)
await client.zones.rulesets.phases.entrypoint.update("http_request_firewall_managed", {
zone_id: zoneId,
rules: [{ expression: "true", action: "execute", action_parameters: { id: wafRulesetId } }],
});
// Layer 3: Rate Limiting (abuse prevention)
await client.zones.rulesets.phases.entrypoint.update("http_ratelimit", {
zone_id: zoneId,
rules: [{ expression: "http.request.uri.path eq \"/api/login\"", action: "block", ratelimit: { characteristics: ["ip.src"], period: 60, requests_per_period: 5 } }],
});
// Layer 4: Bot Management (automation detection)
await client.zones.rulesets.phases.entrypoint.update("http_request_sbfm", {
zone_id: zoneId,
rules: [{ expression: "cf.bot_management.score lt 30", action: "managed_challenge" }],
});
```
## Cache Strategy for DDoS Mitigation
Exclude query strings from cache key to counter randomized query parameter attacks.
```typescript
const cacheRule = {
expression: "http.request.uri.path matches \"^/api/\"",
action: "set_cache_settings",
action_parameters: {
cache: true,
cache_key: { ignore_query_strings_order: true, custom_key: { query_string: { exclude: { all: true } } } },
},
};
await client.zones.rulesets.phases.entrypoint.update("http_request_cache_settings", { zone_id: zoneId, rules: [cacheRule] });
```
**Rationale**: Attackers randomize query strings (`?random=123456`) to bypass cache. Excluding query params ensures cache hits absorb attack traffic.
See [configuration.md](./configuration.md) for rule structure details.

View File

@@ -0,0 +1,75 @@
# Cloudflare Durable Objects Storage
Persistent storage API for Durable Objects with SQLite and KV backends, PITR, and automatic concurrency control.
## Overview
DO Storage provides:
- SQLite-backed (recommended) or KV-backed
- SQL API + synchronous/async KV APIs
- Automatic input/output gates (race-free)
- 30-day point-in-time recovery (PITR)
- Transactions and alarms
**Use cases:** Stateful coordination, real-time collaboration, counters, sessions, rate limiters
**Billing:** Charged by request, GB-month storage, and rowsRead/rowsWritten for SQL operations
## Quick Start
```typescript
export class Counter extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
this.sql.exec('CREATE TABLE IF NOT EXISTS data(key TEXT PRIMARY KEY, value INTEGER)');
}
async increment(): Promise<number> {
const result = this.sql.exec(
'INSERT INTO data VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = value + 1 RETURNING value',
'counter', 1
).one();
return result?.value || 1;
}
}
```
## Storage Backends
| Backend | Create Method | APIs | PITR |
|---------|---------------|------|------|
| SQLite (recommended) | `new_sqlite_classes` | SQL + sync KV + async KV | ✅ |
| KV (legacy) | `new_classes` | async KV only | ❌ |
## Core APIs
- **SQL API** (`ctx.storage.sql`): Full SQLite with extensions (FTS5, JSON, math)
- **Sync KV** (`ctx.storage.kv`): Synchronous key-value (SQLite only)
- **Async KV** (`ctx.storage`): Asynchronous key-value (both backends)
- **Transactions** (`transactionSync()`, `transaction()`)
- **PITR** (`getBookmarkForTime()`, `onNextSessionRestoreBookmark()`)
- **Alarms** (`setAlarm()`, `alarm()` handler)
## Reading Order
**New to DO storage:** configuration.md → api.md → patterns.md → gotchas.md
**Building features:** patterns.md → api.md → gotchas.md
**Debugging issues:** gotchas.md → api.md
**Writing tests:** testing.md
## In This Reference
- [configuration.md](./configuration.md) - wrangler.jsonc migrations, SQLite vs KV setup, RPC binding
- [api.md](./api.md) - SQL exec/cursors, KV methods, storage options, transactions, alarms, PITR
- [patterns.md](./patterns.md) - Schema migrations, caching, rate limiting, batch processing, parent-child coordination
- [gotchas.md](./gotchas.md) - Concurrency gates, INTEGER precision, transaction rules, SQL limits
- [testing.md](./testing.md) - vitest-pool-workers setup, testing DOs with SQL/alarms/PITR
## See Also
- [durable-objects](../durable-objects/) - DO fundamentals and coordination patterns
- [workers](../workers/) - Worker runtime for DO stubs
- [d1](../d1/) - Shared database alternative to per-DO storage

View File

@@ -0,0 +1,102 @@
# DO Storage API Reference
## SQL API
```typescript
const cursor = this.sql.exec('SELECT * FROM users WHERE email = ?', email);
for (let row of cursor) {} // Objects: { id, name, email }
cursor.toArray(); cursor.one(); // Single row (throws if != 1)
for (let row of cursor.raw()) {} // Arrays: [1, "Alice", "..."]
// Manual iteration
const iter = cursor[Symbol.iterator]();
const first = iter.next(); // { value: {...}, done: false }
cursor.columnNames; // ["id", "name", "email"]
cursor.rowsRead; cursor.rowsWritten; // Billing
type User = { id: number; name: string; email: string };
const user = this.sql.exec<User>('...', userId).one();
```
## Sync KV API (SQLite only)
```typescript
this.ctx.storage.kv.get("counter"); // undefined if missing
this.ctx.storage.kv.put("counter", 42);
this.ctx.storage.kv.put("user", { name: "Alice", age: 30 });
this.ctx.storage.kv.delete("counter"); // true if existed
for (let [key, value] of this.ctx.storage.kv.list()) {}
// List options: start, prefix, reverse, limit
this.ctx.storage.kv.list({ start: "user:", prefix: "user:", reverse: true, limit: 100 });
```
## Async KV API (Both backends)
```typescript
await this.ctx.storage.get("key"); // Single
await this.ctx.storage.get(["key1", "key2"]); // Multiple (max 128)
await this.ctx.storage.put("key", value); // Single
await this.ctx.storage.put({ "key1": "v1", "key2": { nested: true } }); // Multiple (max 128)
await this.ctx.storage.delete("key");
await this.ctx.storage.delete(["key1", "key2"]);
await this.ctx.storage.list({ prefix: "user:", limit: 100 });
// Options: allowConcurrency, noCache, allowUnconfirmed
await this.ctx.storage.get("key", { allowConcurrency: true, noCache: true });
await this.ctx.storage.put("key", value, { allowUnconfirmed: true, noCache: true });
```
### Storage Options
| Option | Methods | Effect | Use Case |
|--------|---------|--------|----------|
| `allowConcurrency` | get, list | Skip input gate; allow concurrent requests during read | Read-heavy metrics that don't need strict consistency |
| `noCache` | get, put, list | Skip in-memory cache; always read from disk | Rarely-accessed data or testing storage directly |
| `allowUnconfirmed` | put, delete | Return before write confirms (still protected by output gate) | Non-critical writes where latency matters more than confirmation |
## Transactions
```typescript
// Sync (SQL/sync KV only)
this.ctx.storage.transactionSync(() => {
this.sql.exec('UPDATE accounts SET balance = balance - ? WHERE id = ?', 100, 1);
this.sql.exec('UPDATE accounts SET balance = balance + ? WHERE id = ?', 100, 2);
return "result";
});
// Async
await this.ctx.storage.transaction(async () => {
const value = await this.ctx.storage.get("counter");
await this.ctx.storage.put("counter", value + 1);
if (value > 100) this.ctx.storage.rollback(); // Explicit rollback
});
```
## Point-in-Time Recovery
```typescript
await this.ctx.storage.getCurrentBookmark();
await this.ctx.storage.getBookmarkForTime(Date.now() - 2 * 24 * 60 * 60 * 1000);
await this.ctx.storage.onNextSessionRestoreBookmark(bookmark);
this.ctx.abort(); // Restart to apply; bookmarks lexically comparable (earlier < later)
```
## Alarms
```typescript
await this.ctx.storage.setAlarm(Date.now() + 60000); // Timestamp or Date
await this.ctx.storage.getAlarm();
await this.ctx.storage.deleteAlarm();
async alarm() { await this.doScheduledWork(); }
```
## Misc
```typescript
await this.ctx.storage.deleteAll(); // Atomic for SQLite; alarm NOT included
this.ctx.storage.sql.databaseSize; // Bytes
```

View File

@@ -0,0 +1,112 @@
# DO Storage Configuration
## SQLite-backed (Recommended)
**wrangler.jsonc:**
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["Counter", "Session", "RateLimiter"]
}
]
}
```
**Migration lifecycle:** Migrations run once per deployment. Existing DO instances get new storage backend on next invocation. Renaming/removing classes requires `renamed_classes` or `deleted_classes` entries.
## KV-backed (Legacy)
**wrangler.jsonc:**
```jsonc
{
"migrations": [
{
"tag": "v1",
"new_classes": ["OldCounter"]
}
]
}
```
## TypeScript Setup
```typescript
export class MyDurableObject extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Initialize schema
this.sql.exec(`
CREATE TABLE IF NOT EXISTS users(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
`);
}
}
// Binding
interface Env {
MY_DO: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.MY_DO.idFromName('singleton');
const stub = env.MY_DO.get(id);
// Modern RPC: call methods directly (recommended)
const result = await stub.someMethod();
return Response.json(result);
// Legacy: forward request (still works)
// return stub.fetch(request);
}
}
```
## CPU Limits
```jsonc
{
"limits": {
"cpu_ms": 300000 // 5 minutes (default 30s)
}
}
```
## Location Control
```typescript
// Jurisdiction (GDPR/FedRAMP)
const euNamespace = env.MY_DO.jurisdiction("eu");
const id = euNamespace.newUniqueId();
const stub = euNamespace.get(id);
// Location hint (best effort)
const stub = env.MY_DO.get(id, { locationHint: "enam" });
// Hints: wnam, enam, sam, weur, eeur, apac, oc, afr, me
```
## Initialization
```typescript
export class Counter extends DurableObject {
value: number;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Block concurrent requests during init
ctx.blockConcurrencyWhile(async () => {
this.value = (await ctx.storage.get("value")) || 0;
});
}
}
```

View File

@@ -0,0 +1,150 @@
# DO Storage Gotchas & Troubleshooting
## Concurrency Model (CRITICAL)
Durable Objects use **input/output gates** to prevent race conditions:
### Input Gates
Block new requests during storage reads from CURRENT request:
```typescript
// SAFE: Input gate active during await
async increment() {
const val = await this.ctx.storage.get("counter"); // Input gate blocks other requests
await this.ctx.storage.put("counter", val + 1);
return val;
}
```
### Output Gates
Hold response until ALL writes from current request confirm:
```typescript
// SAFE: Output gate waits for put() to confirm before returning response
async increment() {
const val = await this.ctx.storage.get("counter");
this.ctx.storage.put("counter", val + 1); // No await
return new Response(String(val)); // Response delayed until write confirms
}
```
### Write Coalescing
Multiple writes to same key = atomic (last write wins):
```typescript
// SAFE: All three writes coalesce atomically
this.ctx.storage.put("key", 1);
this.ctx.storage.put("key", 2);
this.ctx.storage.put("key", 3); // Final value: 3
```
### Breaking Gates (DANGER)
**fetch() breaks input/output gates** → allows request interleaving:
```typescript
// UNSAFE: fetch() allows another request to interleave
async unsafe() {
const val = await this.ctx.storage.get("counter");
await fetch("https://api.example.com"); // Gate broken!
await this.ctx.storage.put("counter", val + 1); // Race condition possible
}
```
**Solution:** Use `blockConcurrencyWhile()` or `transaction()`:
```typescript
// SAFE: Block concurrent requests explicitly
async safe() {
return await this.ctx.blockConcurrencyWhile(async () => {
const val = await this.ctx.storage.get("counter");
await fetch("https://api.example.com");
await this.ctx.storage.put("counter", val + 1);
return val;
});
}
```
### allowConcurrency Option
Opt out of input gate for reads that don't need protection:
```typescript
// Allow concurrent reads (no consistency guarantee)
const val = await this.ctx.storage.get("metrics", { allowConcurrency: true });
```
## Common Errors
### "Race Condition in Concurrent Calls"
**Cause:** Multiple concurrent storage operations initiated from same event (e.g., `Promise.all()`) are not protected by input gate
**Solution:** Avoid concurrent storage operations within single event; input gate only serializes requests from different events, not operations within same event
### "Direct SQL Transaction Statements"
**Cause:** Using `BEGIN TRANSACTION` directly instead of transaction methods
**Solution:** Use `this.ctx.storage.transactionSync()` for sync operations or `this.ctx.storage.transaction()` for async operations
### "Async in transactionSync"
**Cause:** Using async operations inside `transactionSync()` callback
**Solution:** Use async `transaction()` method instead of `transactionSync()` when async operations needed
### "TypeScript Type Mismatch at Runtime"
**Cause:** Query doesn't return all fields specified in TypeScript type
**Solution:** Ensure SQL query selects all columns that match the TypeScript type definition
### "Silent Data Corruption with Large IDs"
**Cause:** JavaScript numbers have 53-bit precision; SQLite INTEGER is 64-bit
**Symptom:** IDs > 9007199254740991 (Number.MAX_SAFE_INTEGER) silently truncate/corrupt
**Solution:** Store large IDs as TEXT:
```typescript
// BAD: Snowflake/Twitter IDs will corrupt
this.sql.exec("CREATE TABLE events(id INTEGER PRIMARY KEY)");
this.sql.exec("INSERT INTO events VALUES (?)", 1234567890123456789n); // Corrupts!
// GOOD: Store as TEXT
this.sql.exec("CREATE TABLE events(id TEXT PRIMARY KEY)");
this.sql.exec("INSERT INTO events VALUES (?)", "1234567890123456789");
```
### "Alarm Not Deleted with deleteAll()"
**Cause:** `deleteAll()` doesn't delete alarms automatically
**Solution:** Call `deleteAlarm()` explicitly before `deleteAll()` to remove alarm
### "Slow Performance"
**Cause:** Using async KV API instead of sync API
**Solution:** Use sync KV API (`ctx.storage.kv`) for better performance with simple key-value operations
### "High Billing from Storage Operations"
**Cause:** Excessive `rowsRead`/`rowsWritten` or unused objects not cleaned up
**Solution:** Monitor `rowsRead`/`rowsWritten` metrics and ensure unused objects call `deleteAll()`
### "Durable Object Overloaded"
**Cause:** Single DO exceeding ~1K req/sec soft limit
**Solution:** Shard across multiple DOs with random IDs or other distribution strategy
## Limits
| Limit | Value | Notes |
|-------|-------|-------|
| Max columns per table | 100 | SQL limitation |
| Max string/BLOB per row | 2 MB | SQL limitation |
| Max row size | 2 MB | SQL limitation |
| Max SQL statement size | 100 KB | SQL limitation |
| Max SQL parameters | 100 | SQL limitation |
| Max LIKE/GLOB pattern | 50 B | SQL limitation |
| SQLite storage per object | 10 GB | SQLite-backed storage |
| SQLite key+value size | 2 MB | SQLite-backed storage |
| KV storage per object | Unlimited | KV-style storage |
| KV key size | 2 KiB | KV-style storage |
| KV value size | 128 KiB | KV-style storage |
| Request throughput | ~1K req/sec | Soft limit per DO |

View File

@@ -0,0 +1,182 @@
# DO Storage Patterns & Best Practices
## Schema Migration
```typescript
export class MyDurableObject extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
// Use SQLite's built-in user_version pragma
const ver = this.sql.exec("PRAGMA user_version").one()?.user_version || 0;
if (ver === 0) {
this.sql.exec(`CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT)`);
this.sql.exec("PRAGMA user_version = 1");
}
if (ver === 1) {
this.sql.exec(`ALTER TABLE users ADD COLUMN email TEXT`);
this.sql.exec("PRAGMA user_version = 2");
}
}
}
```
## In-Memory Caching
```typescript
export class UserCache extends DurableObject {
cache = new Map<string, User>();
async getUser(id: string): Promise<User | undefined> {
if (this.cache.has(id)) {
const cached = this.cache.get(id);
if (cached) return cached;
}
const user = await this.ctx.storage.get<User>(`user:${id}`);
if (user) this.cache.set(id, user);
return user;
}
async updateUser(id: string, data: Partial<User>) {
const updated = { ...await this.getUser(id), ...data };
this.cache.set(id, updated);
await this.ctx.storage.put(`user:${id}`, updated);
return updated;
}
}
```
## Rate Limiting
```typescript
export class RateLimiter extends DurableObject {
async checkLimit(key: string, limit: number, window: number): Promise<boolean> {
const now = Date.now();
this.sql.exec('DELETE FROM requests WHERE key = ? AND timestamp < ?', key, now - window);
const count = this.sql.exec('SELECT COUNT(*) as count FROM requests WHERE key = ?', key).one().count;
if (count >= limit) return false;
this.sql.exec('INSERT INTO requests (key, timestamp) VALUES (?, ?)', key, now);
return true;
}
}
```
## Batch Processing with Alarms
```typescript
export class BatchProcessor extends DurableObject {
pending: string[] = [];
async addItem(item: string) {
this.pending.push(item);
if (!await this.ctx.storage.getAlarm()) await this.ctx.storage.setAlarm(Date.now() + 5000);
}
async alarm() {
const items = [...this.pending];
this.pending = [];
this.sql.exec(`INSERT INTO processed_items (item, timestamp) VALUES ${items.map(() => "(?, ?)").join(", ")}`, ...items.flatMap(item => [item, Date.now()]));
}
}
```
## Initialization Pattern
```typescript
export class Counter extends DurableObject {
value: number;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => { this.value = (await ctx.storage.get("value")) || 0; });
}
async increment() {
this.value++;
this.ctx.storage.put("value", this.value); // Don't await (output gate protects)
return this.value;
}
}
```
## Safe Counter / Optimized Write
```typescript
// Input gate blocks other requests
async getUniqueNumber(): Promise<number> {
let val = await this.ctx.storage.get("counter");
await this.ctx.storage.put("counter", val + 1);
return val;
}
// No await on write - output gate delays response until write confirms
async increment(): Promise<Response> {
let val = await this.ctx.storage.get("counter");
this.ctx.storage.put("counter", val + 1);
return new Response(String(val));
}
```
## Parent-Child Coordination
Hierarchical DO pattern where parent manages child DOs:
```typescript
// Parent DO coordinates children
export class Workspace extends DurableObject {
async createDocument(name: string): Promise<string> {
const docId = crypto.randomUUID();
const childId = this.env.DOCUMENT.idFromName(`${this.ctx.id.toString()}:${docId}`);
const childStub = this.env.DOCUMENT.get(childId);
await childStub.initialize(name);
// Track child in parent storage
this.sql.exec('INSERT INTO documents (id, name, created) VALUES (?, ?, ?)',
docId, name, Date.now());
return docId;
}
async listDocuments(): Promise<string[]> {
return this.sql.exec('SELECT id FROM documents').toArray().map(r => r.id);
}
}
// Child DO
export class Document extends DurableObject {
async initialize(name: string) {
this.sql.exec('CREATE TABLE IF NOT EXISTS content(key TEXT PRIMARY KEY, value TEXT)');
this.sql.exec('INSERT INTO content VALUES (?, ?)', 'name', name);
}
}
```
## Write Coalescing Pattern
Multiple writes to same key coalesce atomically (last write wins):
```typescript
async updateMetrics(userId: string, actions: Action[]) {
// All writes coalesce - no await needed
for (const action of actions) {
this.ctx.storage.put(`user:${userId}:lastAction`, action.type);
this.ctx.storage.put(`user:${userId}:count`,
await this.ctx.storage.get(`user:${userId}:count`) + 1);
}
// Output gate ensures all writes confirm before response
return new Response("OK");
}
// Atomic batch with SQL
async batchUpdate(items: Item[]) {
this.sql.exec('BEGIN');
for (const item of items) {
this.sql.exec('INSERT OR REPLACE INTO items VALUES (?, ?)', item.id, item.value);
}
this.sql.exec('COMMIT');
}
```
## Cleanup
```typescript
async cleanup() {
await this.ctx.storage.deleteAlarm(); // Separate from deleteAll
await this.ctx.storage.deleteAll();
}
```

View File

@@ -0,0 +1,183 @@
# DO Storage Testing
Testing Durable Objects with storage using `vitest-pool-workers`.
## Setup
**vitest.config.ts:**
```typescript
import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";
export default defineWorkersConfig({
test: {
poolOptions: {
workers: { wrangler: { configPath: "./wrangler.toml" } }
}
}
});
```
**package.json:** Add `@cloudflare/vitest-pool-workers` and `vitest` to devDependencies
## Basic Testing
```typescript
import { env, runInDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("Counter DO", () => {
it("increments counter", async () => {
const id = env.COUNTER.idFromName("test");
const result = await runInDurableObject(env.COUNTER, id, async (instance, state) => {
const val1 = await instance.increment();
const val2 = await instance.increment();
return { val1, val2 };
});
expect(result.val1).toBe(1);
expect(result.val2).toBe(2);
});
});
```
## Testing SQL Storage
```typescript
it("creates and queries users", async () => {
const id = env.USER_MANAGER.idFromName("test");
await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => {
await instance.createUser("alice@example.com", "Alice");
const user = await instance.getUser("alice@example.com");
expect(user).toEqual({ email: "alice@example.com", name: "Alice" });
});
});
it("handles schema migrations", async () => {
const id = env.USER_MANAGER.idFromName("migration-test");
await runInDurableObject(env.USER_MANAGER, id, async (instance, state) => {
const version = state.storage.sql.exec(
"SELECT value FROM _meta WHERE key = 'schema_version'"
).one()?.value;
expect(version).toBe("1");
});
});
```
## Testing Alarms
```typescript
import { runDurableObjectAlarm } from "cloudflare:test";
it("processes batch on alarm", async () => {
const id = env.BATCH_PROCESSOR.idFromName("test");
// Add items
await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance) => {
await instance.addItem("item1");
await instance.addItem("item2");
});
// Trigger alarm
await runDurableObjectAlarm(env.BATCH_PROCESSOR, id);
// Verify processed
await runInDurableObject(env.BATCH_PROCESSOR, id, async (instance, state) => {
const count = state.storage.sql.exec(
"SELECT COUNT(*) as count FROM processed_items"
).one().count;
expect(count).toBe(2);
});
});
```
## Testing Concurrency
```typescript
it("handles concurrent increments safely", async () => {
const id = env.COUNTER.idFromName("concurrent-test");
// Parallel increments
const results = await Promise.all([
runInDurableObject(env.COUNTER, id, (i) => i.increment()),
runInDurableObject(env.COUNTER, id, (i) => i.increment()),
runInDurableObject(env.COUNTER, id, (i) => i.increment())
]);
// All should get unique values
expect(new Set(results).size).toBe(3);
expect(Math.max(...results)).toBe(3);
});
```
## Test Isolation
```typescript
// Per-test unique IDs
let testId: string;
beforeEach(() => { testId = crypto.randomUUID(); });
it("isolated test", async () => {
const id = env.MY_DO.idFromName(testId);
// Uses unique DO instance
});
// Cleanup pattern
it("with cleanup", async () => {
const id = env.MY_DO.idFromName("cleanup-test");
try {
await runInDurableObject(env.MY_DO, id, async (instance) => {});
} finally {
await runInDurableObject(env.MY_DO, id, async (instance, state) => {
await state.storage.deleteAll();
});
}
});
```
## Testing PITR
```typescript
it("restores from bookmark", async () => {
const id = env.MY_DO.idFromName("pitr-test");
// Create checkpoint
const bookmark = await runInDurableObject(env.MY_DO, id, async (instance, state) => {
await state.storage.put("value", 1);
return await state.storage.getCurrentBookmark();
});
// Modify and restore
await runInDurableObject(env.MY_DO, id, async (instance, state) => {
await state.storage.put("value", 2);
await state.storage.onNextSessionRestoreBookmark(bookmark);
state.abort();
});
// Verify restored
await runInDurableObject(env.MY_DO, id, async (instance, state) => {
const value = await state.storage.get("value");
expect(value).toBe(1);
});
});
```
## Testing Transactions
```typescript
it("rolls back on error", async () => {
const id = env.BANK.idFromName("transaction-test");
await runInDurableObject(env.BANK, id, async (instance, state) => {
await state.storage.put("balance", 100);
await expect(
state.storage.transaction(async () => {
await state.storage.put("balance", 50);
throw new Error("Cancel");
})
).rejects.toThrow("Cancel");
const balance = await state.storage.get("balance");
expect(balance).toBe(100); // Rolled back
});
});
```

View File

@@ -0,0 +1,185 @@
# Cloudflare Durable Objects
Expert guidance for building stateful applications with Cloudflare Durable Objects.
## Reading Order
1. **First time?** Read this overview + Quick Start
2. **Setting up?** See [Configuration](./configuration.md)
3. **Building features?** Use decision trees below → [Patterns](./patterns.md)
4. **Debugging issues?** Check [Gotchas](./gotchas.md)
5. **Deep dive?** [API](./api.md) and [DO Storage](../do-storage/README.md)
## Overview
Durable Objects combine compute with storage in globally-unique, strongly-consistent packages:
- **Globally unique instances**: Each DO has unique ID for multi-client coordination
- **Co-located storage**: Fast, strongly-consistent storage with compute
- **Automatic placement**: Objects spawn near first request location
- **Stateful serverless**: In-memory state + persistent storage
- **Single-threaded**: Serial request processing (no race conditions)
## Rules of Durable Objects
Critical rules preventing most production issues:
1. **One alarm per DO** - Schedule multiple events via queue pattern
2. **~1K req/s per DO max** - Shard for higher throughput
3. **Constructor runs every wake** - Keep initialization light; use lazy loading
4. **Hibernation clears memory** - In-memory state lost; persist critical data
5. **Use `ctx.waitUntil()` for cleanup** - Ensures completion after response sent
6. **No setTimeout for persistence** - Use `setAlarm()` for reliable scheduling
## Core Concepts
### Class Structure
All DOs extend `DurableObject` base class with constructor receiving `DurableObjectState` (storage, WebSockets, alarms) and `Env` (bindings).
### Lifecycle States
```
[Not Created] → [Active] ⇄ [Hibernated] → [Evicted]
[Destroyed]
```
- **Not Created**: DO ID exists but instance never spawned
- **Active**: Processing requests, in-memory state valid, billed per GB-hour
- **Hibernated**: WebSocket connections open but zero compute, zero cost
- **Evicted**: Removed from memory; next request triggers cold start
- **Destroyed**: Data deleted via migration or manual deletion
### Accessing from Workers
Workers use bindings to get stubs, then call RPC methods directly (recommended) or use fetch handler (legacy).
**RPC vs fetch() decision:**
```
├─ New project + compat ≥2024-04-03 → RPC (type-safe, simpler)
├─ Need HTTP semantics (headers, status) → fetch()
├─ Proxying requests to DO → fetch()
└─ Legacy compatibility → fetch()
```
See [Patterns: RPC vs fetch()](./patterns.md) for examples.
### ID Generation
- `idFromName()`: Deterministic, named coordination (rate limiting, locks)
- `newUniqueId()`: Random IDs for sharding high-throughput workloads
- `idFromString()`: Derive from existing IDs
- Jurisdiction option: Data locality compliance
### Storage Options
**Which storage API?**
```
├─ Structured data, relations, transactions → SQLite (recommended)
├─ Simple KV on SQLite DO → ctx.storage.kv (sync KV)
└─ Legacy KV-only DO → ctx.storage (async KV)
```
- **SQLite** (recommended): Structured data, transactions, 10GB/DO
- **Synchronous KV API**: Simple key-value on SQLite objects
- **Asynchronous KV API**: Legacy/advanced use cases
See [DO Storage](../do-storage/README.md) for deep dive.
### Special Features
- **Alarms**: Schedule future execution per-DO (1 per DO - use queue pattern for multiple)
- **WebSocket Hibernation**: Zero-cost idle connections (memory cleared on hibernation)
- **Point-in-Time Recovery**: Restore to any point in 30 days (SQLite only)
## Quick Start
```typescript
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject<Env> {
async increment(): Promise<number> {
const result = this.ctx.storage.sql.exec(
`INSERT INTO counters (id, value) VALUES (1, 1)
ON CONFLICT(id) DO UPDATE SET value = value + 1
RETURNING value`
).one();
return result.value;
}
}
// Worker access
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const id = env.COUNTER.idFromName("global");
const stub = env.COUNTER.get(id);
const count = await stub.increment();
return new Response(`Count: ${count}`);
}
};
```
## Decision Trees
### What do you need?
```
├─ Coordinate requests (rate limit, lock, session)
│ → idFromName(identifier) → [Patterns: Rate Limiting/Locks](./patterns.md)
├─ High throughput (>1K req/s)
│ → Sharding with newUniqueId() or hash → [Patterns: Sharding](./patterns.md)
├─ Real-time updates (WebSocket, chat, collab)
│ → WebSocket hibernation + room pattern → [Patterns: Real-time](./patterns.md)
├─ Background work (cleanup, notifications, scheduled tasks)
│ → Alarms + queue pattern (1 alarm/DO) → [Patterns: Multiple Events](./patterns.md)
└─ User sessions with expiration
→ Session pattern + alarm cleanup → [Patterns: Session Management](./patterns.md)
```
### Which access pattern?
```
├─ New project + typed methods → RPC (compat ≥2024-04-03)
├─ Need HTTP semantics → fetch()
├─ Proxying to DO → fetch()
└─ Legacy compat → fetch()
```
See [Patterns: RPC vs fetch()](./patterns.md) for examples.
### Which storage?
```
├─ Structured data, SQL queries, transactions → SQLite (recommended)
├─ Simple KV on SQLite DO → ctx.storage.kv (sync API)
└─ Legacy KV-only DO → ctx.storage (async API)
```
See [DO Storage](../do-storage/README.md) for complete guide.
## Essential Commands
```bash
npx wrangler dev # Local dev with DOs
npx wrangler dev --remote # Test against prod DOs
npx wrangler deploy # Deploy + auto-apply migrations
```
## Resources
**Docs**: https://developers.cloudflare.com/durable-objects/
**API Reference**: https://developers.cloudflare.com/durable-objects/api/
**Examples**: https://developers.cloudflare.com/durable-objects/examples/
## In This Reference
- **[Configuration](./configuration.md)** - wrangler.jsonc setup, migrations, bindings, environments
- **[API](./api.md)** - Class structure, ctx methods, alarms, WebSocket hibernation
- **[Patterns](./patterns.md)** - Sharding, rate limiting, locks, real-time, sessions
- **[Gotchas](./gotchas.md)** - Limits, hibernation caveats, common errors
## See Also
- **[DO Storage](../do-storage/README.md)** - SQLite, KV, transactions (detailed storage guide)
- **[Workers](../workers/README.md)** - Core Workers runtime features
- **[WebSockets](../websockets/README.md)** - WebSocket APIs and patterns

View File

@@ -0,0 +1,187 @@
# Durable Objects API
## Class Structure
```typescript
import { DurableObject } from "cloudflare:workers";
export class MyDO extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Runs on EVERY wake - keep light!
}
// RPC methods (called directly from worker)
async myMethod(arg: string): Promise<string> { return arg; }
// fetch handler (legacy/HTTP semantics)
async fetch(req: Request): Promise<Response> { /* ... */ }
// Lifecycle handlers
async alarm() { /* alarm fired */ }
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) { /* ... */ }
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { /* ... */ }
async webSocketError(ws: WebSocket, error: unknown) { /* ... */ }
}
```
## DurableObjectState Context Methods
### Concurrency Control
```typescript
// Complete work after response sent (e.g., cleanup, logging)
this.ctx.waitUntil(promise: Promise<any>): void
// Critical section - blocks all other requests until complete
await this.ctx.blockConcurrencyWhile(async () => {
// No other requests processed during this block
// Use for initialization or critical operations
})
```
**When to use:**
- `waitUntil()`: Background cleanup, logging, non-critical work after response
- `blockConcurrencyWhile()`: First-time init, schema migration, critical state setup
### Lifecycle
```typescript
this.ctx.id // DurableObjectId of this instance
this.ctx.abort() // Force eviction (use after PITR restore to reload state)
```
### Storage Access
```typescript
this.ctx.storage.sql // SQLite API (recommended)
this.ctx.storage.kv // Sync KV API (SQLite DOs only)
this.ctx.storage // Async KV API (legacy/KV-only DOs)
```
See **[DO Storage](../do-storage/README.md)** for complete storage API reference.
### WebSocket Management
```typescript
this.ctx.acceptWebSocket(ws: WebSocket, tags?: string[]) // Enable hibernation
this.ctx.getWebSockets(tag?: string): WebSocket[] // Get by tag or all
this.ctx.getTags(ws: WebSocket): string[] // Get tags for connection
```
### Alarms
```typescript
await this.ctx.storage.setAlarm(timestamp: number | Date) // Schedule (overwrites existing)
await this.ctx.storage.getAlarm(): number | null // Get next alarm time
await this.ctx.storage.deleteAlarm(): void // Cancel alarm
```
**Limit:** 1 alarm per DO. Use queue pattern for multiple events (see [Patterns](./patterns.md)).
## Storage APIs
For detailed storage documentation including SQLite queries, KV operations, transactions, and Point-in-Time Recovery, see **[DO Storage](../do-storage/README.md)**.
Quick reference:
```typescript
// SQLite (recommended)
this.ctx.storage.sql.exec("SELECT * FROM users WHERE id = ?", userId).one()
// Sync KV (SQLite DOs only)
this.ctx.storage.kv.get("key")
// Async KV (legacy)
await this.ctx.storage.get("key")
```
## Alarms
Schedule future work that survives eviction:
```typescript
// Set alarm (overwrites any existing alarm)
await this.ctx.storage.setAlarm(Date.now() + 3600000) // 1 hour from now
await this.ctx.storage.setAlarm(new Date("2026-02-01")) // Absolute time
// Check next alarm
const nextRun = await this.ctx.storage.getAlarm() // null if none
// Cancel alarm
await this.ctx.storage.deleteAlarm()
// Handler called when alarm fires
async alarm() {
// Runs once alarm triggers
// DO wakes from hibernation if needed
// Use for cleanup, notifications, scheduled tasks
}
```
**Limitations:**
- 1 alarm per DO maximum
- Overwrites previous alarm when set
- Use queue pattern for multiple scheduled events (see [Patterns](./patterns.md))
**Reliability:**
- Alarms survive DO eviction/restart
- Cloudflare retries failed alarms automatically
- Not guaranteed exactly-once (handle idempotently)
## WebSocket Hibernation
Hibernation allows DOs with open WebSocket connections to consume zero compute/memory until message arrives.
```typescript
async fetch(req: Request): Promise<Response> {
const [client, server] = Object.values(new WebSocketPair());
this.ctx.acceptWebSocket(server, ["room:123"]); // Tags for filtering
server.serializeAttachment({ userId: "abc" }); // Persisted metadata
return new Response(null, { status: 101, webSocket: client });
}
// Called when message arrives (DO wakes from hibernation)
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
const data = ws.deserializeAttachment(); // Retrieve metadata
for (const c of this.ctx.getWebSockets("room:123")) c.send(msg);
}
// Called on close (optional handler)
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
// Cleanup logic, remove from lists, etc.
}
// Called on error (optional handler)
async webSocketError(ws: WebSocket, error: unknown) {
console.error("WebSocket error:", error);
// Handle error, close connection, etc.
}
```
**Key concepts:**
- **Auto-hibernation:** DO hibernates when no active requests/alarms
- **Zero cost:** Hibernated DOs incur no charges while preserving connections
- **Memory cleared:** All in-memory state lost on hibernation
- **Attachment persistence:** Use `serializeAttachment()` for per-connection metadata that survives hibernation
- **Tags for filtering:** Group connections by room/channel/user for targeted broadcasts
**Handler lifecycle:**
- `webSocketMessage`: DO wakes, processes message, may hibernate after
- `webSocketClose`: Called when client closes (optional - implement for cleanup)
- `webSocketError`: Called on connection error (optional - implement for error handling)
**Metadata persistence:**
```typescript
// Store connection metadata (survives hibernation)
ws.serializeAttachment({ userId: "abc", room: "lobby" })
// Retrieve after hibernation
const { userId, room } = ws.deserializeAttachment()
```
## See Also
- **[DO Storage](../do-storage/README.md)** - Complete storage API reference
- **[Patterns](./patterns.md)** - Real-world usage patterns
- **[Gotchas](./gotchas.md)** - Hibernation caveats and limits

View File

@@ -0,0 +1,160 @@
# Durable Objects Configuration
## Basic Setup
```jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01", // Use latest; ≥2024-04-03 for RPC
"durable_objects": {
"bindings": [
{
"name": "MY_DO", // Env binding name
"class_name": "MyDO" // Class exported from this worker
},
{
"name": "EXTERNAL", // Access DO from another worker
"class_name": "ExternalDO",
"script_name": "other-worker"
}
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] } // Prefer SQLite
]
}
```
## Binding Options
```jsonc
{
"name": "BINDING_NAME",
"class_name": "ClassName",
"script_name": "other-worker", // Optional: external DO
"environment": "production" // Optional: isolate by env
}
```
## Jurisdiction (Data Locality)
Specify jurisdiction at ID creation for data residency compliance:
```typescript
// EU data residency
const id = env.MY_DO.idFromName("user:123", { jurisdiction: "eu" })
// Available jurisdictions
const jurisdictions = ["eu", "fedramp"] // More may be added
// All operations on this DO stay within jurisdiction
const stub = env.MY_DO.get(id)
await stub.someMethod() // Data stays in EU
```
**Key points:**
- Set at ID creation time, immutable afterward
- DO instance physically located within jurisdiction
- Storage and compute guaranteed within boundary
- Use for GDPR, FedRAMP, other compliance requirements
- No cross-jurisdiction access (requests fail if DO in different jurisdiction)
## Migrations
```jsonc
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["MyDO"] }, // Create SQLite (recommended)
// { "tag": "v1", "new_classes": ["MyDO"] }, // Create KV (paid only)
{ "tag": "v2", "renamed_classes": [{ "from": "Old", "to": "New" }] },
{ "tag": "v3", "transferred_classes": [{ "from": "Src", "from_script": "old", "to": "Dest" }] },
{ "tag": "v4", "deleted_classes": ["Obsolete"] } // Destroys ALL data!
]
}
```
**Migration rules:**
- Tags must be unique and sequential (v1, v2, v3...)
- No rollback supported (test with `--dry-run` first)
- Auto-applied on deploy
- `new_sqlite_classes` recommended over `new_classes` (SQLite vs KV)
- `deleted_classes` immediately destroys ALL data (irreversible)
## Environment Isolation
Separate DO namespaces per environment (staging/production have distinct object instances):
```jsonc
{
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "MyDO" }]
},
"env": {
"production": {
"durable_objects": {
"bindings": [
{ "name": "MY_DO", "class_name": "MyDO", "environment": "production" }
]
}
}
}
}
```
Deploy: `npx wrangler deploy --env production`
## Limits & Settings
```jsonc
{
"limits": {
"cpu_ms": 300000 // Max CPU time: 30s default, 300s max
}
}
```
See [Gotchas](./gotchas.md) for complete limits table.
## Types
```typescript
import { DurableObject } from "cloudflare:workers";
interface Env {
MY_DO: DurableObjectNamespace<MyDO>;
}
export class MyDO extends DurableObject<Env> {}
type DurableObjectNamespace<T> = {
newUniqueId(options?: { jurisdiction?: string }): DurableObjectId;
idFromName(name: string): DurableObjectId;
idFromString(id: string): DurableObjectId;
get(id: DurableObjectId): DurableObjectStub<T>;
};
```
## Commands
```bash
# Development
npx wrangler dev # Local dev
npx wrangler dev --remote # Test against production DOs
# Deployment
npx wrangler deploy # Deploy + auto-apply migrations
npx wrangler deploy --dry-run # Validate migrations without deploying
npx wrangler deploy --env production
# Management
npx wrangler durable-objects list # List namespaces
npx wrangler durable-objects info <namespace> <id> # Inspect specific DO
npx wrangler durable-objects delete <namespace> <id> # Delete DO (destroys data)
```
## See Also
- **[API](./api.md)** - DurableObjectState and lifecycle handlers
- **[Patterns](./patterns.md)** - Multi-environment patterns
- **[Gotchas](./gotchas.md)** - Migration caveats, limits

View File

@@ -0,0 +1,197 @@
# Durable Objects Gotchas
## Common Errors
### "Hibernation Cleared My In-Memory State"
**Problem:** Variables lost after hibernation
**Cause:** DO auto-hibernates when idle; in-memory state not persisted
**Solution:** Use `ctx.storage` for critical data, `ws.serializeAttachment()` for per-connection metadata
```typescript
// ❌ Wrong - lost on hibernation
private userCount = 0;
async webSocketMessage(ws: WebSocket, msg: string) {
this.userCount++; // Lost!
}
// ✅ Right - persisted
async webSocketMessage(ws: WebSocket, msg: string) {
const count = this.ctx.storage.kv.get("userCount") || 0;
this.ctx.storage.kv.put("userCount", count + 1);
}
```
### "setTimeout Didn't Fire After Restart"
**Problem:** Scheduled work lost on eviction
**Cause:** `setTimeout` in-memory only; eviction clears timers
**Solution:** Use `ctx.storage.setAlarm()` for reliable scheduling
```typescript
// ❌ Wrong - lost on eviction
setTimeout(() => this.cleanup(), 3600000);
// ✅ Right - survives eviction
await this.ctx.storage.setAlarm(Date.now() + 3600000);
async alarm() { await this.cleanup(); }
```
### "Constructor Runs on Every Wake"
**Problem:** Expensive init logic slows all requests
**Cause:** Constructor runs on every wake (first request after eviction OR after hibernation)
**Solution:** Lazy initialization or cache in storage
**Critical understanding:** Constructor runs in two scenarios:
1. **Cold start** - DO evicted from memory, first request creates new instance
2. **Wake from hibernation** - DO with WebSockets hibernated, message/alarm wakes it
```typescript
// ❌ Wrong - expensive on every wake
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.heavyData = this.loadExpensiveData(); // Slow!
}
// ✅ Right - lazy load
private heavyData?: HeavyData;
private getHeavyData() {
if (!this.heavyData) this.heavyData = this.loadExpensiveData();
return this.heavyData;
}
```
### "Durable Object Overloaded (503 errors)"
**Problem:** 503 errors under load
**Cause:** Single DO exceeding ~1K req/s throughput limit
**Solution:** Shard across multiple DOs (see [Patterns: Sharding](./patterns.md))
### "Storage Quota Exceeded (Write failures)"
**Problem:** Write operations failing
**Cause:** DO storage exceeding 10GB limit or account quota
**Solution:** Cleanup with alarms, use `deleteAll()` for old data, upgrade plan
### "CPU Time Exceeded (Terminated)"
**Problem:** Request terminated mid-execution
**Cause:** Processing exceeding 30s CPU time default limit
**Solution:** Increase `limits.cpu_ms` in wrangler.jsonc (max 300s) or chunk work
### "WebSockets Disconnect on Eviction"
**Problem:** Connections drop unexpectedly
**Cause:** DO evicted from memory without hibernation API
**Solution:** Use WebSocket hibernation handlers + client reconnection logic
### "Migration Failed (Deploy error)"
**Cause:** Non-unique tags, non-sequential tags, or invalid class names in migration
**Solution:** Check tag uniqueness/sequential ordering and verify class names are correct
### "RPC Method Not Found"
**Cause:** compatibility_date < 2024-04-03 preventing RPC usage
**Solution:** Update compatibility_date to >= 2024-04-03 or use fetch() instead of RPC
### "Only One Alarm Allowed"
**Cause:** Need multiple scheduled tasks but only one alarm supported per DO
**Solution:** Use event queue pattern to schedule multiple tasks with single alarm
### "Race Condition Despite Single-Threading"
**Problem:** Concurrent requests see inconsistent state
**Cause:** Async operations allow request interleaving (await = yield point)
**Solution:** Use `blockConcurrencyWhile()` for critical sections or atomic storage ops
```typescript
// ❌ Wrong - race condition
async incrementCounter() {
const count = await this.ctx.storage.get("count") || 0;
// ⚠️ Another request could execute here during await
await this.ctx.storage.put("count", count + 1);
}
// ✅ Right - atomic operation
async incrementCounter() {
return this.ctx.storage.sql.exec(
"INSERT INTO counters (id, value) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET value = value + 1 RETURNING value"
).one().value;
}
// ✅ Right - explicit locking
async criticalOperation() {
await this.ctx.blockConcurrencyWhile(async () => {
const count = await this.ctx.storage.get("count") || 0;
await this.ctx.storage.put("count", count + 1);
});
}
```
### "Migration Rollback Not Supported"
**Cause:** Attempting to rollback a migration after deployment
**Solution:** Test with `--dry-run` before deploying; migrations cannot be rolled back
### "deleted_classes Destroys Data"
**Problem:** Migration deleted all data
**Cause:** `deleted_classes` migration immediately destroys all DO instances and data
**Solution:** Test with `--dry-run`; use `transferred_classes` to preserve data during moves
### "Cold Starts Are Slow"
**Problem:** First request after eviction takes longer
**Cause:** DO constructor + initial storage access on cold start
**Solution:** Expected behavior; optimize constructor, use connection pooling in clients, consider warming strategy for critical DOs
```typescript
// Warming strategy (periodically ping critical DOs)
export default {
async scheduled(event: ScheduledEvent, env: Env) {
const criticalIds = ["auth", "sessions", "locks"];
await Promise.all(criticalIds.map(name => {
const id = env.MY_DO.idFromName(name);
const stub = env.MY_DO.get(id);
return stub.ping(); // Keep warm
}));
}
};
```
## Limits
| Limit | Free | Paid | Notes |
|-------|------|------|-------|
| SQLite storage per DO | 10 GB | 10 GB | Per Durable Object instance |
| SQLite total storage | 5 GB | Unlimited | Account-wide quota |
| Key+value size | 2 MB | 2 MB | Single KV pair (SQLite/async) |
| CPU time default | 30s | 30s | Per request; configurable |
| CPU time max | 300s | 300s | Set via `limits.cpu_ms` |
| DO classes | 100 | 500 | Distinct DO class definitions |
| SQL columns | 100 | 100 | Per table |
| SQL statement size | 100 KB | 100 KB | Max SQL query size |
| WebSocket message size | 32 MiB | 32 MiB | Per message |
| Request throughput | ~1K req/s | ~1K req/s | Per DO (soft limit - shard for more) |
| Alarms per DO | 1 | 1 | Use queue pattern for multiple events |
| Total DOs | Unlimited | Unlimited | Create as many instances as needed |
| WebSockets | Unlimited | Unlimited | Within 128MB memory limit per DO |
| Memory per DO | 128 MB | 128 MB | In-memory state + WebSocket buffers |
## Hibernation Caveats
1. **Memory cleared** - All in-memory variables lost; reconstruct from storage or `deserializeAttachment()`
2. **Constructor reruns** - Runs on wake; avoid expensive operations, use lazy initialization
3. **No guarantees** - DO may evict instead of hibernate; design for both
4. **Attachment limit** - `serializeAttachment()` data must be JSON-serializable, keep small
5. **Alarm wakes DO** - Alarm prevents hibernation until handler completes
6. **WebSocket state not automatic** - Must explicitly persist with `serializeAttachment()` or storage
## See Also
- **[Patterns](./patterns.md)** - Workarounds for common limitations
- **[API](./api.md)** - Storage limits and quotas
- **[Configuration](./configuration.md)** - Setting CPU limits

View File

@@ -0,0 +1,201 @@
# Durable Objects Patterns
## When to Use Which Pattern
| Need | Pattern | ID Strategy |
|------|---------|-------------|
| Rate limit per user/IP | Rate Limiting | `idFromName(identifier)` |
| Mutual exclusion | Distributed Lock | `idFromName(resource)` |
| >1K req/s throughput | Sharding | `newUniqueId()` or hash |
| Real-time updates | WebSocket Collab | `idFromName(room)` |
| User sessions | Session Management | `idFromName(sessionId)` |
| Background cleanup | Alarm-based | Any |
## RPC vs fetch()
**RPC** (compat ≥2024-04-03): Type-safe, simpler, default for new projects
**fetch()**: Legacy compat, HTTP semantics, proxying
```typescript
const count = await stub.increment(); // RPC
const count = await (await stub.fetch(req)).json(); // fetch()
```
## Sharding (High Throughput)
Single DO ~1K req/s max. Shard for higher throughput:
```typescript
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const userId = new URL(req.url).searchParams.get("user");
const hash = hashCode(userId) % 100; // 100 shards
const id = env.COUNTER.idFromName(`shard:${hash}`);
return env.COUNTER.get(id).fetch(req);
}
};
function hashCode(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash) + str.charCodeAt(i);
return Math.abs(hash);
}
```
**Decisions:**
- **Shard count**: 10-1000 typical (start with 100, measure, adjust)
- **Shard key**: User ID, IP, session - must distribute evenly (use hash)
- **Aggregation**: Coordinator DO or external system (D1, R2)
## Rate Limiting
```typescript
async checkLimit(key: string, limit: number, windowMs: number): Promise<boolean> {
const req = this.ctx.storage.sql.exec("SELECT COUNT(*) as count FROM requests WHERE key = ? AND timestamp > ?", key, Date.now() - windowMs).one();
if (req.count >= limit) return false;
this.ctx.storage.sql.exec("INSERT INTO requests (key, timestamp) VALUES (?, ?)", key, Date.now());
return true;
}
```
## Distributed Lock
```typescript
private held = false;
async acquire(timeoutMs = 5000): Promise<boolean> {
if (this.held) return false;
this.held = true;
await this.ctx.storage.setAlarm(Date.now() + timeoutMs);
return true;
}
async release() { this.held = false; await this.ctx.storage.deleteAlarm(); }
async alarm() { this.held = false; } // Auto-release on timeout
```
## Hibernation-Aware Pattern
Preserve state across hibernation:
```typescript
async fetch(req: Request): Promise<Response> {
const [client, server] = Object.values(new WebSocketPair());
const userId = new URL(req.url).searchParams.get("user");
server.serializeAttachment({ userId }); // Survives hibernation
this.ctx.acceptWebSocket(server, ["room:lobby"]);
server.send(JSON.stringify({ type: "init", state: this.ctx.storage.kv.get("state") }));
return new Response(null, { status: 101, webSocket: client });
}
async webSocketMessage(ws: WebSocket, msg: string) {
const { userId } = ws.deserializeAttachment(); // Retrieve after wake
const state = this.ctx.storage.kv.get("state") || {};
state[userId] = JSON.parse(msg);
this.ctx.storage.kv.put("state", state);
for (const c of this.ctx.getWebSockets("room:lobby")) c.send(msg);
}
```
## Real-time Collaboration
Broadcast updates to all connected clients:
```typescript
async webSocketMessage(ws: WebSocket, msg: string) {
const data = JSON.parse(msg);
this.ctx.storage.kv.put("doc", data.content); // Persist
for (const c of this.ctx.getWebSockets()) if (c !== ws) c.send(msg); // Broadcast
}
```
### WebSocket Reconnection
**Client-side** (exponential backoff):
```typescript
class ResilientWS {
private delay = 1000;
connect(url: string) {
const ws = new WebSocket(url);
ws.onclose = () => setTimeout(() => {
this.connect(url);
this.delay = Math.min(this.delay * 2, 30000);
}, this.delay);
}
}
```
**Server-side** (cleanup on close):
```typescript
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
const { userId } = ws.deserializeAttachment();
this.ctx.storage.sql.exec("UPDATE users SET online = false WHERE id = ?", userId);
for (const c of this.ctx.getWebSockets()) c.send(JSON.stringify({ type: "user_left", userId }));
}
```
## Session Management
```typescript
async createSession(userId: string, data: object): Promise<string> {
const id = crypto.randomUUID(), exp = Date.now() + 86400000;
this.ctx.storage.sql.exec("INSERT INTO sessions VALUES (?, ?, ?, ?)", id, userId, JSON.stringify(data), exp);
await this.ctx.storage.setAlarm(exp);
return id;
}
async getSession(id: string): Promise<object | null> {
const row = this.ctx.storage.sql.exec("SELECT data FROM sessions WHERE id = ? AND expires_at > ?", id, Date.now()).one();
return row ? JSON.parse(row.data) : null;
}
async alarm() { this.ctx.storage.sql.exec("DELETE FROM sessions WHERE expires_at <= ?", Date.now()); }
```
## Multiple Events (Single Alarm)
Queue pattern to schedule multiple events:
```typescript
async scheduleEvent(id: string, runAt: number) {
await this.ctx.storage.put(`event:${id}`, { id, runAt });
const curr = await this.ctx.storage.getAlarm();
if (!curr || runAt < curr) await this.ctx.storage.setAlarm(runAt);
}
async alarm() {
const events = await this.ctx.storage.list({ prefix: "event:" }), now = Date.now();
let next = null;
for (const [key, ev] of events) {
if (ev.runAt <= now) {
await this.processEvent(ev);
await this.ctx.storage.delete(key);
} else if (!next || ev.runAt < next) next = ev.runAt;
}
if (next) await this.ctx.storage.setAlarm(next);
}
```
## Graceful Cleanup
Use `ctx.waitUntil()` to complete work after response:
```typescript
async myMethod() {
const response = { success: true };
this.ctx.waitUntil(this.ctx.storage.sql.exec("DELETE FROM old_data WHERE timestamp < ?", cutoff));
return response;
}
```
## Best Practices
- **Design**: Use `idFromName()` for coordination, `newUniqueId()` for sharding, minimize constructor work
- **Storage**: Prefer SQLite, batch with transactions, set alarms for cleanup, use PITR before risky ops
- **Performance**: ~1K req/s per DO max - shard for more, cache in memory, use alarms for deferred work
- **Reliability**: Handle 503 with retry+backoff, design for cold starts, test migrations with `--dry-run`
- **Security**: Validate inputs in Workers, rate limit DO creation, use jurisdiction for compliance
## See Also
- **[API](./api.md)** - ctx methods, WebSocket handlers
- **[Gotchas](./gotchas.md)** - Hibernation caveats, common errors
- **[DO Storage](../do-storage/README.md)** - Storage patterns and transactions

View File

@@ -0,0 +1,89 @@
# Cloudflare Email Routing Skill Reference
## Overview
Cloudflare Email Routing enables custom email addresses for your domain that route to verified destination addresses. It's free, privacy-focused (no storage/access), and includes Email Workers for programmatic email processing.
**Available to all Cloudflare customers using Cloudflare as authoritative nameserver.**
## Quick Start
```typescript
// Basic email handler
export default {
async email(message, env, ctx) {
// CRITICAL: Must consume stream before response
const parser = new PostalMime.default();
const email = await parser.parse(await message.raw.arrayBuffer());
// Process email
console.log(`From: ${message.from}, Subject: ${email.subject}`);
// Forward or reject
await message.forward("verified@destination.com");
}
} satisfies ExportedHandler<Env>;
```
## Reading Order
**Start here based on your goal:**
1. **New to Email Routing?** → [configuration.md](configuration.md) → [patterns.md](patterns.md)
2. **Adding Workers?** → [api.md](api.md) § Worker Runtime API → [patterns.md](patterns.md)
3. **Sending emails?** → [api.md](api.md) § SendEmail Binding
4. **Managing via API?** → [api.md](api.md) § REST API Operations
5. **Debugging issues?** → [gotchas.md](gotchas.md)
## Decision Tree
```
Need to receive emails?
├─ Simple forwarding only? → Dashboard rules (configuration.md)
├─ Complex logic/filtering? → Email Workers (api.md + patterns.md)
└─ Parse attachments/body? → postal-mime library (patterns.md § Parse Email)
Need to send emails?
├─ From Worker? → SendEmail binding (api.md § SendEmail)
└─ From external app? → Use external SMTP/API service
Having issues?
├─ Email not arriving? → gotchas.md § Mail Authentication
├─ Worker crashing? → gotchas.md § Stream Consumption
└─ Forward failing? → gotchas.md § Destination Verification
```
## Key Concepts
**Routing Rules**: Pattern-based forwarding configured via Dashboard/API. Simple but limited.
**Email Workers**: Custom TypeScript handlers with full email access. Handles complex logic, parsing, storage, rejection.
**SendEmail Binding**: Outbound email API for Workers. Transactional email only (no marketing/bulk).
**ForwardableEmailMessage**: Runtime interface for incoming emails. Provides headers, raw stream, forward/reject methods.
## In This Reference
- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config
- **[api.md](api.md)** - REST API + Worker runtime API + types
- **[patterns.md](patterns.md)** - Common patterns with working examples
- **[gotchas.md](gotchas.md)** - Critical pitfalls, troubleshooting, limits
## Architecture
```
Internet → MX Records → Cloudflare Email Routing
├─ Routing Rules (dashboard)
└─ Email Worker (your code)
├─ Forward to destination
├─ Reject with reason
├─ Store in R2/KV/D1
└─ Send outbound (SendEmail)
```
## See Also
- [Cloudflare Docs: Email Routing](https://developers.cloudflare.com/email-routing/)
- [Cloudflare Docs: Email Workers](https://developers.cloudflare.com/email-routing/email-workers/)
- [postal-mime npm package](https://www.npmjs.com/package/postal-mime)

View File

@@ -0,0 +1,195 @@
# Email Routing API Reference
## Worker Runtime API
### Email Handler Interface
```typescript
interface ExportedHandler<Env = unknown> {
email?(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): void | Promise<void>;
}
```
### ForwardableEmailMessage
Main interface for incoming emails:
```typescript
interface ForwardableEmailMessage {
readonly from: string; // Envelope sender (e.g., "sender@example.com")
readonly to: string; // Envelope recipient (e.g., "you@yourdomain.com")
readonly headers: Headers; // Web API Headers object
readonly raw: ReadableStream; // Raw MIME message stream
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<void>;
}
```
**Key Properties:**
| Property | Type | Description |
|----------|------|-------------|
| `from` | `string` | Envelope sender (MAIL FROM), not header From |
| `to` | `string` | Envelope recipient (RCPT TO), not header To |
| `headers` | `Headers` | Email headers (Subject, From, To, etc.) |
| `raw` | `ReadableStream` | Raw MIME message (consume once only) |
**Methods:**
- `setReject(reason)`: Reject email with bounce message
- `forward(rcptTo, headers?)`: Forward to verified destination, optionally add headers
### Headers Object
Standard Web API Headers interface:
```typescript
// Access headers
const subject = message.headers.get("subject");
const from = message.headers.get("from");
const messageId = message.headers.get("message-id");
// Check spam score
const spamScore = parseFloat(message.headers.get("x-cf-spamh-score") || "0");
if (spamScore > 5) {
message.setReject("Spam detected");
}
```
### Common Headers
`subject`, `from`, `to`, `x-cf-spamh-score` (spam score), `message-id` (deduplication), `dkim-signature` (auth)
### Envelope vs Header Addresses
**Critical distinction:**
```typescript
// Envelope addresses (routing, auth checks)
message.from // "bounce@sender.com" (actual sender)
message.to // "you@yourdomain.com" (your address)
// Header addresses (display, user-facing)
message.headers.get("from") // "Alice <alice@sender.com>"
message.headers.get("to") // "Bob <you@yourdomain.com>"
```
**Use envelope addresses for:**
- Authentication/SPF checks
- Routing decisions
- Bounce handling
**Use header addresses for:**
- Display to users
- Reply-To logic
- User-facing filtering
## SendEmail Binding
Outbound email API for transactional messages.
### Configuration
```jsonc
// wrangler.jsonc
{
"send_email": [
{ "name": "EMAIL" }
]
}
```
### TypeScript Types
```typescript
interface Env {
EMAIL: SendEmail;
}
interface SendEmail {
send(message: EmailMessage): Promise<void>;
}
interface EmailMessage {
from: string | { name?: string; email: string };
to: string | { name?: string; email: string } | Array<string | { name?: string; email: string }>;
subject: string;
text?: string;
html?: string;
headers?: Headers;
reply_to?: string | { name?: string; email: string };
}
```
### Send Email Example
```typescript
interface Env {
EMAIL: SendEmail;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
await env.EMAIL.send({
from: { name: "Acme Corp", email: "noreply@yourdomain.com" },
to: [
{ name: "Alice", email: "alice@example.com" },
"bob@example.com"
],
subject: "Your order #12345 has shipped",
text: "Track your package at: https://track.example.com/12345",
html: "<p>Track your package at: <a href='https://track.example.com/12345'>View tracking</a></p>",
reply_to: { name: "Support", email: "support@yourdomain.com" }
});
return new Response("Email sent");
}
} satisfies ExportedHandler<Env>;
```
### SendEmail Constraints
- **From address**: Must be on verified domain (your domain with Email Routing enabled)
- **Volume limits**: Transactional only, no bulk/marketing email
- **Rate limits**: 100 emails/minute on Free plan, higher on Paid
- **No attachments**: Use links to hosted files instead
- **No DKIM control**: Cloudflare signs automatically
## REST API Operations
Base URL: `https://api.cloudflare.com/client/v4`
### Authentication
```bash
curl -H "Authorization: Bearer $API_TOKEN" https://api.cloudflare.com/client/v4/...
```
### Key Endpoints
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Enable routing | POST | `/zones/{zone_id}/email/routing/enable` |
| Disable routing | POST | `/zones/{zone_id}/email/routing/disable` |
| List rules | GET | `/zones/{zone_id}/email/routing/rules` |
| Create rule | POST | `/zones/{zone_id}/email/routing/rules` |
| Verify destination | POST | `/zones/{zone_id}/email/routing/addresses` |
| List destinations | GET | `/zones/{zone_id}/email/routing/addresses` |
### Create Routing Rule Example
```bash
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/rules" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"name": "Forward sales",
"matchers": [{"type": "literal", "field": "to", "value": "sales@yourdomain.com"}],
"actions": [{"type": "forward", "value": ["alice@company.com"]}],
"priority": 0
}'
```
Matcher types: `literal` (exact match), `all` (catch-all).

View File

@@ -0,0 +1,186 @@
# Email Routing Configuration
## Wrangler Configuration
### Basic Email Worker
```jsonc
// wrangler.jsonc
{
"name": "email-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"send_email": [{ "name": "EMAIL" }]
}
```
```typescript
// src/index.ts
export default {
async email(message, env, ctx) {
await message.forward("destination@example.com");
}
} satisfies ExportedHandler;
```
### With Storage Bindings
```jsonc
{
"name": "email-processor",
"send_email": [{ "name": "EMAIL" }],
"kv_namespaces": [{ "binding": "KV", "id": "abc123" }],
"r2_buckets": [{ "binding": "R2", "bucket_name": "emails" }],
"d1_databases": [{ "binding": "DB", "database_id": "def456" }]
}
```
```typescript
interface Env {
EMAIL: SendEmail;
KV: KVNamespace;
R2: R2Bucket;
DB: D1Database;
}
```
## Local Development
```bash
npx wrangler dev
# Test with curl
curl -X POST 'http://localhost:8787/__email' \
--header 'content-type: message/rfc822' \
--data 'From: test@example.com
To: you@yourdomain.com
Subject: Test
Body'
```
## Deployment
```bash
npx wrangler deploy
```
**Connect to Email Routing:**
Dashboard: Email > Email Routing > [domain] > Settings > Email Workers > Select worker
API:
```bash
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/settings" \
-H "Authorization: Bearer $API_TOKEN" \
-d '{"enabled": true, "worker": "email-worker"}'
```
## DNS (Auto-Created)
```dns
yourdomain.com. IN MX 1 isaac.mx.cloudflare.net.
yourdomain.com. IN MX 2 linda.mx.cloudflare.net.
yourdomain.com. IN MX 3 amir.mx.cloudflare.net.
yourdomain.com. IN TXT "v=spf1 include:_spf.mx.cloudflare.net ~all"
```
## Secrets & Variables
```bash
# Secrets (encrypted)
npx wrangler secret put API_KEY
# Variables (plain)
# wrangler.jsonc
{ "vars": { "THRESHOLD": "5.0" } }
```
```typescript
interface Env {
API_KEY: string;
THRESHOLD: string;
}
```
## TypeScript Setup
```bash
npm install --save-dev @cloudflare/workers-types
```
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"moduleResolution": "bundler",
"strict": true
}
}
```
```typescript
import type { ForwardableEmailMessage } from "@cloudflare/workers-types";
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
await message.forward("dest@example.com");
}
} satisfies ExportedHandler<Env>;
```
## Dependencies
```bash
npm install postal-mime
```
```typescript
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
console.log(email.subject);
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler;
```
## Multi-Environment
```bash
# wrangler.dev.jsonc
{ "name": "worker-dev", "vars": { "ENV": "dev" } }
# wrangler.prod.jsonc
{ "name": "worker-prod", "vars": { "ENV": "prod" } }
npx wrangler deploy --config wrangler.dev.jsonc
npx wrangler deploy --config wrangler.prod.jsonc
```
## CI/CD (GitHub Actions)
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
```

View File

@@ -0,0 +1,196 @@
# Gotchas & Troubleshooting
## Critical Pitfalls
### Stream Consumption (MOST COMMON)
**Problem:** "stream already consumed" or worker hangs
**Cause:** `message.raw` is `ReadableStream` - consume once only
**Solution:**
```typescript
// ❌ WRONG
const email1 = await parser.parse(await message.raw.arrayBuffer());
const email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS
// ✅ CORRECT
const raw = await message.raw.arrayBuffer();
const email = await parser.parse(raw);
```
Consume `message.raw` immediately before any async operations.
### Destination Verification
**Problem:** Emails not forwarding
**Cause:** Destination unverified
**Solution:** Add destination, check inbox for verification email, click link. Verify status: `GET /zones/{id}/email/routing/addresses`
### Mail Authentication
**Problem:** Legitimate emails rejected
**Cause:** Missing SPF/DKIM/DMARC on sender domain
**Solution:** Configure sender DNS:
```dns
example.com. IN TXT "v=spf1 include:_spf.example.com ~all"
selector._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=..."
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine"
```
### Envelope vs Header
**Problem:** Filtering on wrong address
**Solution:**
```typescript
// Routing/auth: envelope
if (message.from === "trusted@example.com") { }
// Display: headers
const display = message.headers.get("from");
```
### SendEmail Limits
| Issue | Limit | Solution |
|-------|-------|----------|
| From domain | Must own | Use Email Routing domain |
| Volume | ~100/min Free | Upgrade or throttle |
| Attachments | Not supported | Link to R2 |
| Type | Transactional | No bulk |
## Common Errors
### CPU Time Exceeded
**Cause:** Heavy parsing, large emails
**Solution:**
```typescript
const size = parseInt(message.headers.get("content-length") || "0") / 1024 / 1024;
if (size > 20) {
message.setReject("Too large");
return;
}
ctx.waitUntil(expensiveWork());
await message.forward("dest@example.com");
```
### Rule Not Triggering
**Causes:** Priority conflict, matcher error, catch-all override
**Solution:** Check priority (lower=first), verify exact match, confirm destination verified
### Undefined Property
**Cause:** Missing header
**Solution:**
```typescript
// ❌ WRONG
const subj = message.headers.get("subject").toLowerCase();
// ✅ CORRECT
const subj = message.headers.get("subject")?.toLowerCase() || "";
```
## Limits
| Resource | Free | Paid |
|----------|------|------|
| Email size | 25 MB | 25 MB |
| Rules | 200 | 200 |
| Destinations | 200 | 200 |
| CPU time | 10ms | 50ms |
| SendEmail | ~100/min | Higher |
## Debugging
### Local
```bash
npx wrangler dev
curl -X POST 'http://localhost:8787/__email' \
--header 'content-type: message/rfc822' \
--data 'From: test@example.com
To: you@yourdomain.com
Subject: Test
Body'
```
### Production
```bash
npx wrangler tail
```
### Pattern
```typescript
export default {
async email(message, env, ctx) {
try {
console.log("From:", message.from);
await process(message, env);
} catch (err) {
console.error(err);
message.setReject(err.message);
}
}
} satisfies ExportedHandler;
```
## Auth Troubleshooting
### Check Status
```typescript
const auth = message.headers.get("authentication-results") || "";
console.log({
spf: auth.includes("spf=pass"),
dkim: auth.includes("dkim=pass"),
dmarc: auth.includes("dmarc=pass")
});
if (!auth.includes("pass")) {
message.setReject("Failed auth");
return;
}
```
### SPF Issues
**Causes:** Forwarding breaks SPF, too many lookups (>10), missing includes
**Solution:**
```dns
; ✅ Good
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
; ❌ Bad - too many
example.com. IN TXT "v=spf1 include:a.com include:b.com ... ~all"
```
### DMARC Alignment
**Cause:** From domain must match SPF/DKIM domain
## Best Practices
1. Consume `message.raw` immediately
2. Verify destinations
3. Handle missing headers (`?.`)
4. Use envelope for routing
5. Check spam scores
6. Test locally first
7. Use `ctx.waitUntil` for background work
8. Size-check early

View File

@@ -0,0 +1,229 @@
# Common Patterns
## 1. Allowlist/Blocklist
```typescript
// Allowlist
const allowed = ["user@example.com", "trusted@corp.com"];
if (!allowed.includes(message.from)) {
message.setReject("Not allowed");
return;
}
await message.forward("inbox@corp.com");
```
## 2. Parse Email Body
```typescript
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// CRITICAL: Consume stream immediately
const raw = await message.raw.arrayBuffer();
const parser = new PostalMime();
const email = await parser.parse(raw);
console.log({
subject: email.subject,
text: email.text,
html: email.html,
from: email.from.address,
attachments: email.attachments.length
});
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler;
```
## 3. Spam Filter
```typescript
const score = parseFloat(message.headers.get("x-cf-spamh-score") || "0");
if (score > 5) {
message.setReject("Spam detected");
return;
}
await message.forward("inbox@corp.com");
```
## 4. Archive to R2
```typescript
interface Env { R2: R2Bucket; }
export default {
async email(message, env, ctx) {
const raw = await message.raw.arrayBuffer();
const key = `${new Date().toISOString()}-${message.from}.eml`;
await env.R2.put(key, raw, {
httpMetadata: { contentType: "message/rfc822" }
});
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 5. Store Metadata in KV
```typescript
import PostalMime from 'postal-mime';
interface Env { KV: KVNamespace; }
export default {
async email(message, env, ctx) {
const raw = await message.raw.arrayBuffer();
const parser = new PostalMime();
const email = await parser.parse(raw);
const metadata = {
from: email.from.address,
subject: email.subject,
timestamp: new Date().toISOString(),
size: raw.byteLength
};
await env.KV.put(`email:${Date.now()}`, JSON.stringify(metadata));
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 6. Subject-Based Routing
```typescript
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject")?.toLowerCase() || "";
if (subject.includes("[urgent]")) {
await message.forward("oncall@corp.com");
} else if (subject.includes("[billing]")) {
await message.forward("billing@corp.com");
} else if (subject.includes("[support]")) {
await message.forward("support@corp.com");
} else {
await message.forward("general@corp.com");
}
}
} satisfies ExportedHandler;
```
## 7. Auto-Reply
```typescript
interface Env {
EMAIL: SendEmail;
REPLIED: KVNamespace;
}
export default {
async email(message, env, ctx) {
const msgId = message.headers.get("message-id");
if (msgId && await env.REPLIED.get(msgId)) {
await message.forward("archive@corp.com");
return;
}
ctx.waitUntil((async () => {
await env.EMAIL.send({
from: "noreply@yourdomain.com",
to: message.from,
subject: "Re: " + (message.headers.get("subject") || ""),
text: "Thank you. We'll respond within 24h."
});
if (msgId) await env.REPLIED.put(msgId, "1", { expirationTtl: 604800 });
})());
await message.forward("support@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 8. Extract Attachments
```typescript
import PostalMime from 'postal-mime';
interface Env { ATTACHMENTS: R2Bucket; }
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
for (const att of email.attachments) {
const key = `${Date.now()}-${att.filename}`;
await env.ATTACHMENTS.put(key, att.content, {
httpMetadata: { contentType: att.mimeType }
});
}
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 9. Log to D1
```typescript
import PostalMime from 'postal-mime';
interface Env { DB: D1Database; }
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
ctx.waitUntil(
env.DB.prepare("INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)")
.bind(new Date().toISOString(), email.from.address, email.subject || "")
.run()
);
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 10. Multi-Tenant
```typescript
interface Env { TENANTS: KVNamespace; }
export default {
async email(message, env, ctx) {
const subdomain = message.to.split("@")[1].split(".")[0];
const config = await env.TENANTS.get(subdomain, "json") as { forward: string } | null;
if (!config) {
message.setReject("Unknown tenant");
return;
}
await message.forward(config.forward);
}
} satisfies ExportedHandler<Env>;
```
## Summary
| Pattern | Use Case | Storage |
|---------|----------|---------|
| Allowlist | Security | None |
| Parse | Body/attachments | None |
| Spam Filter | Reduce spam | None |
| R2 Archive | Email storage | R2 |
| KV Meta | Analytics | KV |
| Subject Route | Dept routing | None |
| Auto-Reply | Support | KV |
| Attachments | Doc mgmt | R2 |
| D1 Log | Audit trail | D1 |
| Multi-Tenant | SaaS | KV |

View File

@@ -0,0 +1,151 @@
# Cloudflare Email Workers
Process incoming emails programmatically using Cloudflare Workers runtime.
## Overview
Email Workers enable custom email processing logic at the edge. Build spam filters, auto-responders, ticket systems, notification handlers, and more using the same Workers runtime you use for HTTP requests.
**Key capabilities**:
- Process inbound emails with full message access
- Forward to verified destinations
- Send replies with proper threading
- Parse MIME content and attachments
- Integrate with KV, R2, D1, and external APIs
## Quick Start
### Minimal ES Modules Handler
```typescript
export default {
async email(message, env, ctx) {
// Reject spam
if (message.from.includes('spam.com')) {
message.setReject('Blocked');
return;
}
// Forward to inbox
await message.forward('inbox@example.com');
}
};
```
### Core Operations
| Operation | Method | Use Case |
|-----------|--------|----------|
| Forward | `message.forward(to, headers?)` | Route to verified destination |
| Reject | `message.setReject(reason)` | Block with SMTP error |
| Reply | `message.reply(emailMessage)` | Auto-respond with threading |
| Parse | postal-mime library | Extract subject, body, attachments |
## Reading Order
For comprehensive understanding, read files in this order:
1. **README.md** (this file) - Overview and quick start
2. **configuration.md** - Setup, deployment, bindings
3. **api.md** - Complete API reference
4. **patterns.md** - Real-world implementation examples
5. **gotchas.md** - Critical pitfalls and debugging
## In This Reference
| File | Description | Key Topics |
|------|-------------|------------|
| [api.md](./api.md) | Complete API reference | ForwardableEmailMessage, SendEmail bindings, reply() method, postal-mime/mimetext APIs |
| [configuration.md](./configuration.md) | Setup and configuration | wrangler.jsonc, bindings, deployment, dependencies |
| [patterns.md](./patterns.md) | Real-world examples | Allowlists from KV, auto-reply with threading, attachment extraction, webhook notifications |
| [gotchas.md](./gotchas.md) | Pitfalls and debugging | Stream consumption, ctx.waitUntil errors, security, limits |
## Architecture
```
Incoming Email → Email Routing → Email Worker
Process + Decide
┌───────────────┼───────────────┐
↓ ↓ ↓
Forward Reply Reject
```
**Event flow**:
1. Email arrives at your domain
2. Email Routing matches route (e.g., `support@example.com`)
3. Bound Email Worker receives `ForwardableEmailMessage`
4. Worker processes and takes action (forward/reply/reject)
5. Email delivered or rejected based on worker logic
## Key Concepts
### Envelope vs Headers
- **Envelope addresses** (`message.from`, `message.to`): SMTP transport addresses (trusted)
- **Header addresses** (parsed from body): Display addresses (can be spoofed)
Use envelope addresses for security decisions.
### Single-Use Streams
`message.raw` is a ReadableStream that can only be read once. Buffer to ArrayBuffer for multiple uses.
```typescript
// Buffer first
const buffer = await new Response(message.raw).arrayBuffer();
const email = await PostalMime.parse(buffer);
```
See [gotchas.md](./gotchas.md#readablestream-can-only-be-consumed-once) for details.
### Verified Destinations
`forward()` only works with addresses verified in the Cloudflare Email Routing dashboard. Add destinations before deployment.
## Use Cases
- **Spam filtering**: Block based on sender, content, or reputation
- **Auto-responders**: Send acknowledgment replies with threading
- **Ticket creation**: Parse emails and create support tickets
- **Email archival**: Store in KV, R2, or D1
- **Notification routing**: Forward to Slack, Discord, or webhooks
- **Attachment processing**: Extract files to R2 storage
- **Multi-tenant routing**: Route based on recipient subdomain
- **Size filtering**: Reject oversized attachments
## Limits
| Limit | Value |
|-------|-------|
| Max message size | 25 MiB |
| Max routing rules | 200 |
| Max destinations | 200 |
| CPU time (free tier) | 10ms |
| CPU time (paid tier) | 50ms |
See [gotchas.md](./gotchas.md#limits-reference) for complete limits table.
## Prerequisites
Before deploying Email Workers:
1. **Enable Email Routing** in Cloudflare dashboard for your domain
2. **Verify destination addresses** for forwarding
3. **Configure DMARC/SPF** for sending domains (required for replies)
4. **Set up wrangler.jsonc** with SendEmail binding
See [configuration.md](./configuration.md) for detailed setup.
## Service Worker Syntax (Deprecated)
Modern projects should use ES modules format shown above. Service Worker syntax (`addEventListener('email', ...)`) is deprecated but still supported.
## See Also
- [Email Routing Documentation](https://developers.cloudflare.com/email-routing/)
- [Workers Platform](https://developers.cloudflare.com/workers/)
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)
- [postal-mime on npm](https://www.npmjs.com/package/postal-mime)
- [mimetext on npm](https://www.npmjs.com/package/mimetext)

Some files were not shown because too many files have changed in this diff Show More