INTEGRITY Документация

Рекомендации по работе с Workers

Лучшие практики для Workers, основанные на реальных сценариях использования в продакшене, собственном опыте Cloudflare и распространённых проблемах, с которыми сталкивается сообщество разработчиков.

Конфигурация

Поддерживайте актуальность даты совместимости

compatibility_date определяет, какие функции среды выполнения и исправления ошибок доступны вашему Worker. Установка сегодняшней даты в новых проектах обеспечивает доступ к самому актуальному поведению. Периодическое обновление этого значения в существующих проектах даёт доступ к новым API и исправлениям без изменения кода.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

Подробнее см. в Даты совместимости.

Включить nodejs_compat

nodejs_compat флаг совместимости даёт Worker доступ к встроенным модулям Node.js, таким как node:crypto, node:buffer, node:stream, и других. Многие библиотеки зависят от этих модулей, и включение этого флага позволяет избежать непонятных ошибок импорта во время выполнения.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

Подробнее см. в Совместимость с Node.js.

Генерация типов привязок с помощью wrangler types

Не пишите вручную свой Env интерфейс. Выполните wrangler types чтобы сгенерировать файл определения типов, соответствующий вашей текущей конфигурации Wrangler. Это позволяет выявлять несоответствия между конфигурацией и кодом на этапе компиляции, а не при развертывании.

Перезапустите wrangler types каждый раз, когда вы добавляете или переименовываете привязку.

npx wrangler types
src/index.js
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request, env) {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
};
src/index.ts
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
} satisfies ExportedHandler<Env>;

Подробнее см. в wrangler types.

Храните secrets с помощью wrangler secret, а не в исходном коде

Секреты (ключи API, токены, учётные данные баз данных) никогда не должны присутствовать в конфигурации Wrangler или исходном коде. Используйте wrangler secret put чтобы хранить их безопасно и обращаться к ним через env во время выполнения. Для локальной разработки используйте .env файл (и убедитесь, что он находится в вашем .gitignore). Подробнее см. Переменные окружения.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// ✅ Good: non-secret configuration lives in version control
	"vars": {
		"API_BASE_URL": "https://api.example.com",
	},

	// 🔴 Bad: never put secrets here
	// "API_KEY": "sk-live-abc123..."
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[vars]
API_BASE_URL = "https://api.example.com"

Чтобы добавить секрет, выполните следующую команду и введите его значение в интерактивном режиме, когда будет предложено:

npx wrangler secret put API_KEY

Также секреты можно передавать по конвейеру из других инструментов или переменных окружения:

# Pipe from another CLI tool
npx some-cli-tool --get-secret | npx wrangler secret put API_KEY
# Pipe from an environment variable or .env file
echo "$API_KEY" | npx wrangler secret put API_KEY

Подробнее см. в Секреты.

Продуманная настройка окружений

Окружения Wrangler позволяют развернуть один и тот же код в отдельных Workers для production, staging и development. Каждое окружение создаёт отдельный Worker с именем {name}-{env} (например, my-api-production и my-api-staging).

Каждое окружение обрабатывается отдельно. Привязки и vars необходимо объявлять для каждого окружения отдельно, они не наследуются. См. ненаследуемые ключи. Корневой Worker (без суффикса окружения) представляет собой отдельное развёртывание. Если вы не планируете его использовать, не выполняйте развёртывание без указания окружения с помощью --env.

{
	"name": "my-api",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// This binding only applies to the root Worker
	"kv_namespaces": [{ "binding": "CACHE", "id": "dev-kv-id" }],

	"env": {
		// Production environment: deploys as "my-api-production"
		"production": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
			"routes": [
				{ "pattern": "api.example.com/*", "zone_name": "example.com" },
			],
		},
		// Staging environment: deploys as "my-api-staging"
		"staging": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }],
			"routes": [
				{ "pattern": "api-staging.example.com/*", "zone_name": "example.com" },
			],
		},
	},
}
name = "my-api"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[kv_namespaces]]
binding = "CACHE"
id = "dev-kv-id"

[[env.production.kv_namespaces]]
binding = "CACHE"
id = "prod-kv-id"

[[env.production.routes]]
pattern = "api.example.com/*"
zone_name = "example.com"

[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-kv-id"

[[env.staging.routes]]
pattern = "api-staging.example.com/*"
zone_name = "example.com"

С этим файлом конфигурации для развертывания в staging:

npx wrangler deploy --env staging

Подробнее см. в Окружения.

Правильная настройка custom domains или маршрутов

Workers поддерживают два механизма маршрутизации, и каждый из них решает свою задачу:

Самая частая ошибка при работе с маршрутами заключается в отсутствующей DNS-записи. Без проксируемой DNS-записи запросы к имени хоста возвращают ERR_NAME_NOT_RESOLVED и никогда не достигают вашего Worker. Если у вас нет реального источника (origin), добавьте проксируемую AAAA запись, указывающую на 100:: как заполнитель.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// Option 1: Custom domain — Worker is the origin, DNS is managed automatically
	"routes": [{ "pattern": "api.example.com", "custom_domain": true }],

	// Option 2: Route — Worker runs in front of an existing origin
	// Requires a proxied DNS record for shop.example.com
	// "routes": [
	// 	{ "pattern": "shop.example.com/*", "zone_name": "example.com" }
	// ]
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[routes]]
pattern = "api.example.com"
custom_domain = true

Подробнее см. в Маршрутизация.

Обработка запросов и ответов

Потоковая передача тела запроса и ответа

Независимо от ограничений по памяти потоковая передача больших запросов и ответов является рекомендуемой практикой на любом языке. Это снижает пиковое использование памяти и уменьшает время до первого байта. В Workers есть лимит памяти 128 MB, поэтому буферизация всего тела с помощью await response.text() или await request.arrayBuffer() приведёт к сбою вашего Worker при больших объёмах данных.

Для тел запросов, которые вы читаете целиком (JSON payload, загрузка файлов), задавайте максимальный размер перед чтением. Это не позволит клиентам присылать данные, которые вам не нужно обрабатывать.

Передавайте данные потоком через ваш Worker с помощью TransformStream для передачи данных из источника в приёмник без буферизации всего объёма в памяти.

src/index.js
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
};

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
};
src/index.ts
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
} satisfies ExportedHandler<Env>;

Когда нужно объединить несколько ответов (например, при получении данных сразу от нескольких вышестоящих API), передавайте тело каждого ответа последовательно в один writable stream. Это позволяет не буферизовать ответы в памяти.

src/concat.js
export default {
	async fetch(request, env) {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
};
src/concat.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
} satisfies ExportedHandler<Env>;

Подробнее см. в Streams.

Используйте waitUntil для работы после отправки ответа

ctx.waitUntil() позволяет выполнять действия после отправки ответа клиенту, например аналитику, запись в кеш, логирование или уведомления через вебхуки. Благодаря этому ответ остаётся быстрым, а фоновые задачи всё равно выполняются.

Используйте ctx.waitUntil() только для работы, которая не влияет на ответ. Если ответ зависит от этой работы, await его перед возвратом ответа либо передавать ответ потоком по мере выполнения работы. Worker, который всё ещё передаёт тело ответа потоком, остаётся активным без ctx.waitUntil().

Есть две распространённые ошибки: деструктуризация ctx (что приводит к потере this привязку и выбрасывает "Illegal invocation"), и превышение 30-секундного waitUntil() ограничение по времени после отправки ответа или отключения клиента.

src/index.js
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(request, env, ctx) {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
};

// ✅ Good: send the response immediately, do background work after
export default {
	async fetch(request, env, ctx) {
		const data = await processRequest(request);

		ctx.waitUntil(logToAnalytics(env, data));
		ctx.waitUntil(updateCache(env, data));

		return Response.json(data);
	},
};

async function logToAnalytics(env, data) {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env, data) {
	await env.CACHE.put("latest", JSON.stringify(data));
}
src/index.ts
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: send the response immediately, do background work after
export default {
	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);
	},
} satisfies ExportedHandler<Env>;

async function logToAnalytics(env: Env, data: unknown): Promise<void> {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env: Env, data: unknown): Promise<void> {
	await env.CACHE.put("latest", JSON.stringify(data));
}

Подробнее см. в Контекст.

Архитектура

Используйте привязки для сервисов Cloudflare вместо REST API

Некоторые сервисы Cloudflare, такие как R2, KV, D1, Queues и Workflows, доступны как привязки. Привязки представляют собой прямые внутрипроцессные ссылки, которые не требуют сетевого перехода, аутентификации или дополнительной задержки. Использование REST API внутри Worker впустую тратит время и добавляет ненужную сложность.

src/index.js
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request, env) {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
};

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request, env) {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
};
src/index.ts
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
} satisfies ExportedHandler<Env>;

Используйте Queues и Workflows для асинхронных и фоновых задач

Долго выполняющиеся, повторяемые или не срочные задачи не должны блокировать запрос. Используйте Queues и Workflows чтобы вынести работу за пределы критического пути. У них разное назначение:

Используйте Queues, если вам нужно разделить отправителя и получателя сообщений. Queues работают как брокер сообщений: один Worker отправляет сообщение, а другой обрабатывает его позже. Это подходящее решение для fan-out (одно событие запускает сразу несколько обработчиков), для буферизации и группировки сообщений (когда нужно накопить сообщения перед записью в другой сервис), а также для простых фоновых задач в одно действие, таких как отправка письма, вызов webhook или запись в лог. Queues гарантируют доставку с семантикой at-least-once и позволяют настраивать количество повторных попыток для каждого сообщения.

Используйте Workflows, если фоновая работа состоит из нескольких шагов, зависящих друг от друга. Workflows представляют собой движок надёжного выполнения: значение, возвращённое каждым шагом, сохраняется, и если шаг завершится ошибкой, будет повторён только этот шаг, а не всё задание целиком. Это правильный выбор для многошаговых процессов (списать оплату с карты, затем создать отправку, затем отправить подтверждение), долгих задач, которым нужно приостанавливаться и возобновляться (ожидание внешнего события или подтверждения человеком в течение часов или дней через step.waitForEvent()), а также сложную условную логику, когда последующие шаги зависят от результатов предыдущих. Workflows могут выполняться часами, днями или неделями.

Используйте оба варианта вместе когда точка входа с высокой пропускной способностью передаёт данные в сложную обработку. Например, Queue может буферизовать входящие заказы, а потребитель создавать экземпляр Workflow для каждого заказа, требующего многоэтапного выполнения.

src/index.js
export default {
	async fetch(request, env) {
		const order = await request.json();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const order = await request.json<{ id: string; type: string }>();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
} satisfies ExportedHandler<Env>;

Подробнее см. в Queues и Workflows.

Используйте service bindings для взаимодействия между Workers

Если одному Worker нужно вызвать другой, используйте привязки к сервисам вместо отправки HTTP-запроса на публичный URL. Service bindings бесплатны, работают в обход публичного интернета и поддерживают типобезопасный RPC.

src/index.js
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(token) {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request, env) {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
};
src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(
		token: string,
	): Promise<{ userId: string; valid: boolean }> {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
} satisfies ExportedHandler<Env>;

Используйте Hyperdrive для подключений к внешним базам данных

Всегда используйте Hyperdrive при подключении к удалённой базе данных PostgreSQL или MySQL из Worker. Hyperdrive поддерживает региональный пул соединений рядом с вашей базой данных, устраняя затраты на TCP-рукопожатие, согласование TLS и настройку соединения при каждом запросе. Он также кеширует результаты запросов там, где это возможно.

Создайте новый Client при каждом запросе. Hyperdrive управляет базовым пулом, поэтому создание клиента происходит быстро. Требует nodejs_compat для поддержки драйверов баз данных.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<YOUR_HYPERDRIVE_ID>"
src/index.js
import { Client } from "pg";

export default {
	async fetch(request, env) {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request, env) {
		const client = new Client({
			connectionString: "postgres://user:[email protected]:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
};
src/index.ts
import { Client } from "pg";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const client = new Client({
			connectionString: "postgres://user:[email protected]:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
} satisfies ExportedHandler<Env>;

Подробнее см. в Hyperdrive.

Используйте Durable Objects для WebSocket

Обычные Workers могут повышать HTTP-соединения до WebSocket, но у них нет персистентного состояния и гибернации. Если isolate вытесняется, соединение теряется, так как нет постоянного актора, который бы его удерживал. Для надёжных долгоживущих WebSocket-соединений используйте Durable Objects на Hibernation API. Durable Objects удерживают WebSocket соединения открытыми, даже когда объект вытеснен из памяти, и автоматически просыпаются при получении сообщения.

Используйте this.ctx.acceptWebSocket() вместо ws.accept() чтобы включить гибернацию. Используйте setWebSocketAutoResponse для контрольных сообщений ping/pong, которые не выводят объект из спящего режима.

src/index.js
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request, env) {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
};

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx, env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request) {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws, message) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(ws, code, reason, wasClean) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}
src/index.ts
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
} satisfies ExportedHandler<Env>;

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request: Request): Promise<Response> {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(
		ws: WebSocket,
		code: number,
		reason: string,
		wasClean: boolean,
	) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}

Подробнее см. в Рекомендации по WebSocket в Durable Objects.

Используйте Workers Static Assets для новых проектов

Workers Static Assets это рекомендуемый способ развёртывания статических сайтов, одностраничных приложений и full-stack приложений на Cloudflare. Если вы начинаете новый проект, используйте Workers вместо Pages. Pages продолжает работать, но новые функции и оптимизации сосредоточены на Workers.

Для полностью статического сайта укажите assets.directory в результате сборки. Скрипт Worker не требуется. Для full-stack приложения добавьте main точку входа и ASSETS привязку для раздачи статических файлов вместе с вашим API.

{
	// Static site — no Worker script needed
	"name": "my-static-site",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"assets": {
		"directory": "./dist",
	},
}
name = "my-static-site"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[assets]
directory = "./dist"

Подробнее см. в Workers Static Assets.

Observability

Включите Workers Logs и Traces

Workers в продакшене без observability представляют собой чёрный ящик. Включите логи и трассировки перед развёртыванием в продакшен. Когда возникает нестабильная ошибка, для диагностики нужны уже собранные данные.

Включите их в конфигурации Wrangler и используйте head_sampling_rate чтобы контролировать объём и управлять расходами. Частота сэмплирования 1 захватывает всё; для Workers с высокой нагрузкой это значение стоит снизить.

Используйте структурированное JSON-логирование с console.log чтобы журналы можно было искать и фильтровать. Используйте console.error на наличие ошибок и console.warn для предупреждений. Они отображаются с соответствующим уровнем важности в дашборде Workers Observability.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"observability": {
		"enabled": true,
		"logs": {
			// Capture 100% of logs — lower this for high-traffic Workers
			"head_sampling_rate": 1,
		},
		"traces": {
			"enabled": true,
			"head_sampling_rate": 0.01, // Sample 1% of traces
		},
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[observability]
enabled = true

  [observability.logs]
  head_sampling_rate = 1

  [observability.traces]
  enabled = true
  head_sampling_rate = 0.01
src/index.js
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request, env) {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

Подробнее см. в Workers Logs и Трассировки.

Подробнее обо всех доступных инструментах наблюдаемости см. в Workers Observability.

Паттерны кода

Не храните состояние уровня запроса в глобальной области видимости

Workers повторно используют изоляты между запросами. Переменная, установленная в одном запросе, сохраняется и в следующем. Из-за этого возникают утечки данных между запросами, устаревшее состояние и ошибки "Cannot perform I/O on behalf of a different request".

Передавайте состояние через аргументы функции или сохраняйте его в env привязках. Никогда в переменных на уровне модуля.

src/index.js
// 🔴 Bad: global mutable state leaks between requests
let currentUser = null;

const badHandler = {
	async fetch(request, env, ctx) {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
};

// ✅ Good: pass request-scoped data through function arguments
export default {
	async fetch(request, env, ctx) {
		const userId = request.headers.get("X-User-Id");
		const result = await handleRequest(userId, env);

		return Response.json(result);
	},
};

async function handleRequest(userId, env) {
	return { userId };
}
src/index.ts
// 🔴 Bad: global mutable state leaks between requests
let currentUser: string | null = null;

const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: pass request-scoped data 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>;

async function handleRequest(userId: string | null, env: Env): Promise<object> {
	return { userId };
}

Подробнее см. в ошибки Workers.

Всегда используйте await или waitUntil для своих Promise

A Promise который не является await, return, или переданы в ctx.waitUntil() представляет собой незавершённый промис (floating promise). Такие промисы приводят к незаметным ошибкам: потерянным результатам, проглоченным исключениям и незавершённой работе. Среда выполнения Workers может завершить ваш isolate до того, как такой промис выполнится до конца.

Выбор зависит от того, связан ли ответ с выполняемой работой. Используйте await или return для работы, которая должна завершиться до того, как ответ станет корректным. Используйте ctx.waitUntil() для работы, которая может выполняться после отправки ответа и должна завершиться в течение waitUntil() ограничение по времени.

Включить no-floating-promises правило линтера, чтобы отслеживать это на этапе разработки. Если вы используете ESLint, включите @typescript-eslint/no-floating-promises. Если вы используете oxlint, включите typescript/no-floating-promises.

# ESLint (typescript-eslint)
npx eslint --rule '{"@typescript-eslint/no-floating-promises": "error"}' src/

# oxlint
npx oxlint --deny typescript/no-floating-promises src/
src/index.js
export default {
	async fetch(request, env, ctx) {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
};
src/index.ts
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

Безопасность

Используйте Web Crypto для безопасной генерации токенов

Среда выполнения Workers предоставляет Web Crypto API для криптографических операций. Используйте crypto.randomUUID() для уникальных идентификаторов и crypto.getRandomValues() для случайных байтов. Никогда не используйте Math.random() для чего-либо, связанного с безопасностью. Он не является криптографически стойким.

Node.js node:crypto также полностью поддерживается, когда nodejs_compat включен, поэтому вы можете использовать любой API, который предпочитаете вы или используемые вами библиотеки.

src/index.js
export default {
	async fetch(request, env) {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
} satisfies ExportedHandler<Env>;

При сравнении секретных значений (ключи API, токены, HMAC-подписи) используйте crypto.subtle.timingSafeEqual() чтобы предотвратить атаки по времени выполнения (timing side-channel). Не прерывайте сравнение раньше времени при несовпадении длины. Сначала закодируйте оба значения в хеш фиксированного размера.

src/verify.js
async function verifyToken(provided, expected) {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	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);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided, expected) {
	return provided === expected;
}
src/verify.ts
async function verifyToken(
	provided: string,
	expected: string,
): Promise<boolean> {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	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);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided: string, expected: string): boolean {
	return provided === expected;
}

Не используйте passThroughOnException в качестве обработки ошибок

passThroughOnException() представляет собой механизм fail-open, который отправляет запросы на ваш source-сервер, если Worker выбрасывает необработанное исключение. Он может быть полезен при миграции с source-сервера, однако скрывает ошибки и усложняет отладку. Используйте явный try...catch блоков структурированные ответы об ошибках.

src/index.js
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(request, env, ctx) {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
};

// ✅ Good: explicit error handling with structured responses
export default {
	async fetch(request, env, ctx) {
		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 });
		}
	},
};

async function handleRequest(request, env) {
	return { status: "ok" };
}
src/index.ts
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: explicit error handling with structured responses
export default {
	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 });
		}
	},
} satisfies ExportedHandler<Env>;

async function handleRequest(request: Request, env: Env): Promise<object> {
	return { status: "ok" };
}

Разработка и тестирование

Тестирование с помощью @cloudflare/vitest-plugin

@cloudflare/vitest-plugin пакет запускает ваши тесты внутри среды выполнения Workers, предоставляя доступ к реальным привязкам (KV, R2, D1, Durable Objects) во время тестирования. Это позволяет выявлять проблемы, которые упускают тесты на основе Node.js, например неподдерживаемые API или отсутствующие флаги совместимости.

Одна известная проблема: плагин Vitest автоматически внедряет nodejs_compat, поэтому тесты проходят, даже если в конфигурации Wrangler нет этого флага. Всегда проверяйте свой wrangler.jsonc включает nodejs_compat если ваш код зависит от встроенных модулей Node.js.

test/index.test.js
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

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");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});
test/index.test.ts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

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");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});

Подробнее см. в Тестирование с помощью Vitest.