Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/docs/architecture/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,11 @@ list of the people Access admits and the usage notification emails) and `STAGE_A
(default `false`).

- Every Worker config sets `workers_dev: false` and `preview_urls: false`, so the only way in is the
zone routes the guards cover, and caps CPU per request at 200 ms for the Shell and 100 ms for a
vertical (`CLOUDFLARE_WORKER_CPU_MS` in `packages/shared-contracts/tooling/modern-config.ts`).
Nothing has been measured yet; raise a cap when a real request hits it.
zone routes the guards cover, and caps CPU per request at 200 ms for the Shell, 100 ms for a
vertical, and 3000 ms for Catalog and commerce-customer-context (`CLOUDFLARE_WORKER_CPU_MS` in
`packages/shared-contracts/tooling/modern-config.ts`). Each BFF request builds the whole Effect
HTTP API runtime, so CPU grows with the endpoint count, and those two verticals have far more
endpoints than the rest. Raise a cap only when Workers analytics shows real requests hitting it.
- A WAF custom rule `ontos_stage_kill_switch` blocks exactly the placed OntOS stage hostnames. It is
added next to any rules other projects keep in the zone, created disabled, and re-runs keep its
current state. The WAF answers before a Worker runs, so blocked requests are never billed. There is
Expand Down
26 changes: 20 additions & 6 deletions app/packages/shared-contracts/tooling/modern-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,18 @@ export const createCloudflareDataPlaneBindings = (envValue: ModernBuildContext['

/**
* CPU budgets per invocation. Workers Paid includes 30M CPU ms a month, so a runaway request is cut
* off here instead of billed. Nothing has been measured yet: an API vertical does a few database and
* SpiceDB round trips (waiting on I/O is not CPU time), and the Shell also renders SSR, so it gets
* twice the vertical budget. Raise a cap only with a measured p99 from Workers analytics.
* off here instead of billed. An API vertical does a few database and SpiceDB round trips (waiting on
* I/O is not CPU time), and the Shell also renders SSR, so it gets twice the vertical budget.
*
* Every BFF request builds and disposes the whole Effect HTTP API runtime, because workerd ties I/O
* objects to the request that created them, so its CPU cost grows with the API's endpoint count.
* Catalog (about 200 endpoints) and commerce-customer-context (about 115) spend far more than 100 ms
* per request, while the next largest vertical has about 20. Workers analytics on stage (72 h, µs
* rounded to ms): successful requests peaked at 1590 ms (Catalog) and 2015 ms (commerce-customer-context),
* and the requests Cloudflare cut off had already spent up to 2539 ms. `largeApiVertical` is the
* smallest round cap above all of them. Raise a cap only with a measured p99 from Workers analytics.
*/
export const CLOUDFLARE_WORKER_CPU_MS = { shell: 200, vertical: 100 } as const;
export const CLOUDFLARE_WORKER_CPU_MS = { largeApiVertical: 3000, shell: 200, vertical: 100 } as const;

/**
* The data plane plus the cost guards every OntOS Worker carries: it answers only on its reviewed
Expand Down Expand Up @@ -386,14 +393,18 @@ const readCloudflareUnitServiceBindings = (appId: string): readonly CloudflareUn

const createCloudflareDeployment = (
build: ModernBuildContext,
worker: { readonly name: string; readonly unitServiceBindings: readonly CloudflareUnitServiceBinding[] },
worker: {
readonly cpuMs: number;
readonly name: string;
readonly unitServiceBindings: readonly CloudflareUnitServiceBinding[];
},
) =>
build.cloudflareDeployEnabled
? {
deploy: {
worker: {
...createCloudflareWorkerConfig(build.envValue, {
cpuMs: CLOUDFLARE_WORKER_CPU_MS.vertical,
cpuMs: worker.cpuMs,
publicUrlVariable: build.cloudflarePublicUrlEnvironmentVariable,
}),
compatibilityDate: '2026-06-02',
Expand Down Expand Up @@ -467,6 +478,7 @@ export const createModernConfig = <Plugin, BuilderPlugin>({
build,
builderPlugins,
chunkLoadingGlobal,
cloudflareCpuMs = CLOUDFLARE_WORKER_CPU_MS.vertical,
cloudflareWorkerName,
moduleUrl,
plugins,
Expand All @@ -479,6 +491,7 @@ export const createModernConfig = <Plugin, BuilderPlugin>({
build: ModernBuildContext;
builderPlugins?: BuilderPlugin[];
chunkLoadingGlobal: string;
cloudflareCpuMs?: number;
cloudflareWorkerName: string;
moduleUrl: string;
plugins: Plugin[];
Expand Down Expand Up @@ -507,6 +520,7 @@ export const createModernConfig = <Plugin, BuilderPlugin>({
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- This generic optional field retains the public factory's inferred return shape and its position in the emitted configuration.
...(builderPlugins === undefined ? {} : { builderPlugins }),
...createCloudflareDeployment(build, {
cpuMs: cloudflareCpuMs,
name: cloudflareWorkerName,
unitServiceBindings: build.cloudflareDeployEnabled ? readCloudflareUnitServiceBindings(appId) : [],
}),
Expand Down
8 changes: 8 additions & 0 deletions app/scripts/tests/cloudflare-data-plane-bindings.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ it('serves every Worker only on its custom domain, off workers.dev and preview U
).toEqual({
cpu_ms: 200,
});
expect(
createCloudflareWorkerConfig(values, {
cpuMs: CLOUDFLARE_WORKER_CPU_MS.largeApiVertical,
publicUrlVariable: PUBLIC_URL,
}).wrangler.limits,
).toEqual({
cpu_ms: 3000,
});
});

it('refuses a Worker build without its public URL', () => {
Expand Down
2 changes: 1 addition & 1 deletion app/verticals/catalog/modern.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const appDevServerHeaders: NonNullable<NonNullable<NonNullable<AppToolsUserConfi
// Only a Worker build binds the private data plane and carries the cost guards; its IDs are required there and unused elsewhere.
const cloudflareWorkerConfig = cloudflareDeployEnabled
? createCloudflareWorkerConfig(envValue, {
cpuMs: CLOUDFLARE_WORKER_CPU_MS.vertical,
cpuMs: CLOUDFLARE_WORKER_CPU_MS.largeApiVertical,
publicUrlVariable: 'ULTRAMODERN_PUBLIC_URL_CATALOG',
})
: undefined;
Expand Down
2 changes: 2 additions & 0 deletions app/verticals/commerce-customer-context/modern.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { i18nPlugin } from '@modern-js/plugin-i18n';
import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';

import {
CLOUDFLARE_WORKER_CPU_MS,
createModernBuildContext,
createModernConfig,
installGlobalRequire,
Expand Down Expand Up @@ -36,6 +37,7 @@ export default defineConfig(
bffPrefix: COMMERCE_CUSTOMER_CONTEXT_API_PREFIX,
build,
chunkLoadingGlobal: '__ULTRAMODERN_VERTICAL_COMMERCE_CUSTOMER_CONTEXT_LOADED_CHUNKS__',
cloudflareCpuMs: CLOUDFLARE_WORKER_CPU_MS.largeApiVertical,
cloudflareWorkerName,
moduleUrl: import.meta.url,
plugins: [
Expand Down
Loading