Workers Best Practices
CloudflareOfficialPublished by Cloudflare
- Skill
- Guidelines
- Cloud and DevOps
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides best practices for developing and maintaining Cloudflare Workers. It emphasizes using project-specific configurations, generated types, and current documentation to ensure code aligns with the runtime environment. It covers configuration management, observability, platform API usage, and common anti-patterns to avoid during development and code reviews.
What it does
- Provides guidance on maintaining compatibility dates and enabling observability features.
- Outlines patterns for secure secret management and environment variable usage.
- Identifies common coding anti-patterns and suggests preferred implementation alternatives.
- Advises on correct binding access patterns for module handlers and platform classes.
- Recommends using generated types to ensure consistency between configuration and code.
- Offers references for runtime patterns, platform APIs, and configuration settings.
When to use it
- When writing new Cloudflare Workers code.
- During code reviews to identify potential configuration or runtime issues.
- When configuring project settings or updating compatibility dates.
- When troubleshooting runtime behavior or binding access errors.
Advantages
- Promotes consistency between Wrangler configuration and application code.
- Helps prevent common runtime errors related to binding access and serialization.
- Encourages the use of modern, supported platform APIs and patterns.
- Reduces the risk of credential leakage through proper secret handling.
Usage examples
- “How should I handle environment variables and secrets in my Worker?”
- The skill advises using wrangler secret for sensitive data and vars for non-secret configuration, while warning against hardcoding secrets in source or config files.
- “What is the best way to define my Env interface?”
- The skill recommends running wrangler types to generate the interface automatically, rather than hand-writing it, to ensure it stays in sync with the configuration.
- “I am getting an error accessing a binding inside a class. What is wrong?”
- The skill explains that platform classes should access bindings via this.env.X, whereas module export handlers should access them via the env parameter.
- “How do I enable logging for my Worker?”
- The skill provides instructions to enable Workers Logs and Traces by setting observability.enabled and observability.traces.enabled to true in the configuration.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/cloudflare/workers-best-practices/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip workers-best-practices.zip -d .agents/skills
Documentation by Cloudflare
Your knowledge of Cloudflare Workers APIs, types, and configuration may be outdated. Prefer retrieval over pre-training when writing or reviewing Workers code.
Use the project's installed versions, generated types, and Wrangler compatibility settings as the baseline for existing code. Retrieve relevant Cloudflare documentation to verify API, configuration, runtime behavior, and limit claims.
References
Read the sections relevant to the task:
| Reference | When to use it |
|---|---|
| Configuration and observability | Compatibility dates, bindings, generated types, secrets, logs, and traces |
| Runtime patterns | Streaming, promise lifetime, request state, service calls, security, and runtime tests |
| Platform API checks | Handler signatures, platform classes, binding access, and serialization |
For missing evidence, consult Workers best practices or find the affected product in the Cloudflare docs directory. Use the installed Wrangler schema for config fields. A newer type package does not supersede the project's configured target.
Keep Compatibility Dates Current
Use today's date for new Workers. Encourage periodic updates for existing Workers, reviewing compatibility changes and running relevant tests. Assess existing behavior against its configured date and flags; see compatibility guidance.
Enable Observability
Enable Workers Logs and Traces when creating or preparing a Worker for production. Set observability.enabled and observability.traces.enabled to true; the top-level setting alone does not enable traces. Use structured JSON logging and configure sampling for the workload. During reviews, flag missing logs or traces. See the configuration example.
Place Compute Near Regional Databases
For Workers that make multiple sequential queries to one regional database, configure explicit placement matching the database's provider and region before deployment. Ask for missing location information, preserve existing placement choices, and verify handler eligibility and deployed request duration. See database placement.
Anti-Patterns to Flag
| Anti-pattern | Consequence and preferred pattern |
|---|---|
await response.text() or similar buffering on unbounded data | Can exhaust Worker memory; stream large or unbounded bodies. |
| Hardcoded secrets in source or config | Leaks credentials through version control; use Wrangler secrets. |
Math.random() for security-sensitive tokens or IDs | Predictable values; use crypto.randomUUID() or crypto.getRandomValues(). |
Async work started without awaiting, returning, or attaching it to ctx.waitUntil() | Work can be dropped and errors missed; tie it to the request or background-work lifetime. |
| Module-level mutable request state | Leaks data across requests and can cause I/O ownership errors; pass request state explicitly. |
| Cloudflare REST API calls for operations available through Worker bindings | Adds network and authentication overhead; use the available binding. |
ctx.passThroughOnException() used as general error handling | Can conceal Worker failures by forwarding to the origin; use explicit error handling and structured error responses. |
Hand-written Env that duplicates Wrangler bindings | Can drift from configuration; generate binding types with wrangler types. |
| Direct string comparison of secret values | Can expose timing differences; use the Web Crypto comparison pattern. |
Destructuring ctx methods, such as const { waitUntil } = ctx | Loses the receiver; call ctx.waitUntil(...). |
any on Env or handler parameters | Hides binding and handler contract errors; use the project's generated and platform types. |
as unknown as T to force a platform type match | Hides incompatibilities; fix the underlying contract. |
implements used in place of extending a platform base class | Does not inherit runtime behavior, this.ctx, or this.env; use the appropriate base class. |
Unbound env.X in a platform class method | Bindings are available through this.env.X; see binding access patterns. |
| Applying one serialization rule across Queues, Workflow steps, storage, and WebSockets | Can reject valid payloads or accept unsupported ones; check the specific API and encoding. |
Validation
Use the project's existing checks for affected Workers behavior: type-check binding or handler contract changes, and run relevant runtime tests for behavior changes. Preserve required repository checks; a narrow edit does not require a full Workers audit.
Scope
This skill covers Workers-specific best practices and code review. For related topics:
Durable Objects: load the
durable-objectsskillWorkflows: see Rules of Workflows
CLI commands: load the
cf-cliskill, or thewranglerskill in a project with a Wrangler configuration file
Skill feedback
If the user reports that this skill gave incorrect or outdated guidance, or is missing information, read references/skill-feedback.md.
Files
- ATTRIBUTION.mdAdded by AutoGineer880 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md6.1 KiB
- references/configuration.md6.6 KiB
- references/platform-apis.md3.8 KiB
- references/runtime-patterns.md13.5 KiB
- references/skill-feedback.md2.8 KiB
references/runtime-patterns.md
Workers Runtime Patterns
Consult the sections relevant to the affected behavior. Examples show preferred patterns and common mistakes; Retrieve links identify documentation to check when an API, behavior, or limit is uncertain. Doc paths are relative to https://developers.cloudflare.com.
Request and response handling: streaming, memory use, and post-response work
Architecture: bindings, Queues, Workflows, and database connections
Code patterns: request state, promise lifetime, and platform limits
Security: Web Crypto and error handling
Development and testing: tests in the Workers runtime
Request & Response Handling
Stream request and response bodies
Workers have a 128 MB memory limit. Buffering entire bodies with await response.text() or await request.arrayBuffer() crashes on large payloads. Stream data through using TransformStream or pass response.body directly.
Check: any await response.text(), await response.json(), or await response.arrayBuffer() on data that could be large or unbounded. Small, bounded payloads (known-size JSON, config files) are fine to buffer.
Correct — stream through:
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch("https://api.example.com/large-dataset");
return new Response(response.body, response);
}Correct — concatenate multiple streams:
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const urls = ["https://api.example.com/part-1", "https://api.example.com/part-2"];
const { readable, writable } = new TransformStream();
// Track the pipeline promise — don't let it float
ctx.waitUntil((async () => {
for (const url of urls) {
const response = await fetch(url);
if (response.body) {
await response.body.pipeTo(writable, { preventClose: true });
}
}
await writable.close();
})());
return new Response(readable, {
headers: { "Content-Type": "application/octet-stream" },
});
}Anti-pattern:
// Buffers entire body — crashes on large payloads
const response = await fetch("https://api.example.com/large-dataset");
const text = await response.text();
return new Response(text);Retrieve: streaming APIs at /workers/runtime-apis/streams/.
Use Zod 4.5.0 or later
Check: Workers using Zod for runtime validation depend on Zod 4.5.0 or later; older versions retain substantially more heap per schema, so check the installed version when investigating high memory usage or OOMs.
Use waitUntil for work after the response
ctx.waitUntil() performs background work (analytics, cache writes, webhooks) after the response is sent. Keeps response fast. 30-second time limit after response.
Check: background work uses ctx.waitUntil(), not inline await. Do not destructure ctx — it loses the this binding and throws "Illegal invocation".
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const data = await processRequest(request);
ctx.waitUntil(logToAnalytics(env, data));
ctx.waitUntil(updateCache(env, data));
return Response.json(data);
}Anti-pattern:
// Destructuring ctx loses the this binding
const { waitUntil } = ctx; // "Illegal invocation" at runtime
waitUntil(somePromise);Architecture
Use bindings for Cloudflare services, not REST APIs
Bindings (KV, R2, D1, Queues, Workflows) are direct, in-process references — no network hop, no authentication, no extra latency. Using the Cloudflare REST API from a Worker wastes time and adds complexity.
Check: no fetch("https://api.cloudflare.com/client/v4/...") calls for services available as bindings.
// Binding — direct, zero-cost
const object = await env.MY_BUCKET.get("my-file");Anti-pattern:
// REST API from inside a Worker — unnecessary overhead
const response = await fetch(
"https://api.cloudflare.com/client/v4/accounts/.../r2/buckets/.../objects/my-file",
{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } }
);Use Queues and Workflows for async and background work
Long-running, retriable, or non-urgent tasks should not block a request.
Queues: decouple producer from consumer. Fan-out, buffering/batching, simple single-step background jobs. At-least-once delivery.
Workflows: multi-step durable execution. Each step's return value is persisted; only failed steps retry. Can run for hours/days/weeks.
Both together: Queue buffers high-throughput entry, consumer creates Workflow instances for complex processing.
Check: long-running work (email sends, webhooks, multi-step processes) is offloaded to Queues or Workflows, not done inline in the fetch handler.
async fetch(request: Request, env: Env): Promise<Response> {
const order = await request.json<{ id: string; type: string }>();
if (order.type === "simple") {
await env.ORDER_QUEUE.send({ orderId: order.id, action: "send-email" });
} else {
await env.FULFILLMENT_WORKFLOW.create({ params: { orderId: order.id } });
}
return Response.json({ status: "accepted" }, { status: 202 });
}Retrieve: /queues/ and /workflows/ for current APIs. For Workflow-specific rules, see Rules of Workflows.
Use service bindings for Worker-to-Worker communication
Service bindings are zero-cost, bypass the public internet, and support type-safe RPC. Do not call another Worker via its public URL.
Check: Worker-to-Worker calls use env.SERVICE_NAME.method() (RPC) or env.SERVICE_NAME.fetch(), not fetch("https://my-other-worker.example.com/...").
import { WorkerEntrypoint } from "cloudflare:workers";
export class AuthService extends WorkerEntrypoint {
async verifyToken(token: string): Promise<{ userId: string; valid: boolean }> {
return { userId: "user-123", valid: true };
}
}
// Caller Worker
const auth = await env.AUTH_SERVICE.verifyToken(token);Retrieve: verify uncertain WorkerEntrypoint import paths or signatures against the project's target types, consulting current docs when runtime compatibility needs clarification.
Use Hyperdrive for external database connections
Hyperdrive maintains a regional connection pool, eliminating per-request TCP + TLS + auth cost (often 300-500ms). Create a new Client per request — Hyperdrive manages the underlying pool. Requires nodejs_compat.
Check: any new Client() or database connection that uses a direct connection string instead of env.HYPERDRIVE.connectionString.
{
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }]
}import { Client } from "pg";
async fetch(request: Request, env: Env): Promise<Response> {
const client = new Client({ connectionString: env.HYPERDRIVE.connectionString });
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
}Retrieve: /hyperdrive/ for current configuration and supported databases.
For multiple sequential queries to one regional database, follow database placement before deployment. A Hyperdrive binding does not automatically place the Worker near its database.
Code Patterns
Do not store request-scoped state in global scope
Workers reuse isolates across requests. Module-level mutable variables cause cross-request data leaks, stale state, and "Cannot perform I/O on behalf of a different request" errors.
Check: no mutable let/var at module scope that gets assigned inside a handler. Pass state through function arguments.
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const userId = request.headers.get("X-User-Id");
const result = await handleRequest(userId, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;Anti-pattern:
// Module-level mutable state — leaks between requests
let currentUser: string | null = null;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
currentUser = request.headers.get("X-User-Id"); // Visible to next request
// ...
},
};Always await or waitUntil Promises
A Promise that is not awaited, returned, or passed to ctx.waitUntil() is a floating promise. Causes: dropped results, swallowed errors, unfinished work. The runtime may terminate the isolate before it completes.
Check: async calls in the affected execution path are awaited, returned, or attached to the appropriate lifetime. Use the project's existing floating-promise lint check, such as Oxlint's typescript/no-floating-promises, when available and relevant; otherwise inspect the promise paths directly. Adding lint tooling is a separate change, not a prerequisite for reviewing this behavior.
// Correct: await when you need the result
const response = await fetch("https://api.example.com/process", { method: "POST", body: JSON.stringify(data) });
// Correct: waitUntil when you don't need the result before responding
ctx.waitUntil(fetch("https://api.example.com/webhook", { method: "POST", body: JSON.stringify(data) }));Anti-pattern:
// Floating promise — result dropped, error swallowed
fetch("https://api.example.com/webhook", { method: "POST", body: JSON.stringify(data) });Be aware of platform limits
Workers have CPU time limits based on the account plan. Heavy synchronous work — tight loops, large JSON parsing, compute-intensive crypto — can hit the CPU limit and terminate the request.
Check: compute-heavy operations that run synchronously. Consider breaking work into smaller chunks, offloading to Queues/Workflows, or using WebAssembly for CPU-intensive tasks.
Retrieve: current limits at /workers/platform/limits/.
Security
Use Web Crypto for secure token generation
Use crypto.randomUUID() for unique IDs and crypto.getRandomValues() for random bytes. Math.random() is not cryptographically secure.
For comparing secrets (API keys, HMAC signatures), use crypto.subtle.timingSafeEqual(). Hash both values to a fixed size first — do not short-circuit on length mismatch (leaks length via timing).
Check: no Math.random() for security-sensitive values. Secret comparisons use timingSafeEqual with fixed-size hashing.
// Secure random UUID
const sessionId = crypto.randomUUID();
// Secure random bytes
const tokenBytes = new Uint8Array(32);
crypto.getRandomValues(tokenBytes);
const token = Array.from(tokenBytes).map((b) => b.toString(16).padStart(2, "0")).join("");// Constant-time comparison — hash first to avoid length leak
async function verifyToken(provided: string, expected: string): Promise<boolean> {
const encoder = new TextEncoder();
const [providedHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(provided)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}Anti-pattern:
// Predictable — not cryptographically secure
const token = Math.random().toString(36).substring(2);
// Timing side-channel — leaks information about the expected value
return provided === expected;Retrieve: /workers/runtime-apis/web-crypto/ for current API surface.
Explicit error handling over passThroughOnException
passThroughOnException() is a fail-open mechanism that sends requests to the origin when the Worker throws. It hides bugs and makes debugging difficult. Use explicit try/catch with structured error responses.
Check: no ctx.passThroughOnException() calls. Error handling uses try/catch with structured JSON error responses and console.error.
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
try {
const result = await handleRequest(request, env);
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(JSON.stringify({ message: "unhandled error", error: message, path: new URL(request.url).pathname }));
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}Development & Testing
Test with @cloudflare/vitest-pool-workers
Runs tests inside the Workers runtime with real bindings. Catches issues that Node.js-based tests miss.
Known pitfall: the Vitest pool auto-injects nodejs_compat, so tests pass even if your wrangler config is missing the flag. Always confirm your wrangler.jsonc includes nodejs_compat if your code depends on Node.js built-ins.
Check: test setup uses @cloudflare/vitest-pool-workers. Tests cover nullable returns (e.g., KV .get() returning null).
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:test";
describe("KV operations", () => {
it("should store and retrieve a value", async () => {
await env.MY_KV.put("key", "value");
const result = await env.MY_KV.get("key");
expect(result).toBe("value");
});
it("should return null for missing keys", async () => {
const result = await env.MY_KV.get("nonexistent");
expect(result).toBeNull();
});
});Retrieve: /workers/testing/vitest-integration/ for current setup and configuration.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Cloudflare
- Publisher
- Cloudflare (Official)
- Source
- cloudflare/skills · skills/workers-best-practices · commit 6eb6c78
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Cloudflare.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download workers-best-practices.zip