diff --git a/README.md b/README.md index ff30e3f..e9ef669 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Technical write-ups on some of the harder problems I came across. | Paper | Topic | Stack | |---|---|---| -| [Scaling streaming toolsets on Cloudflare](scaling-streaming-toolsets/) | Designing a per-user multi-overlay platform so cost-per-user stays roughly flat as you grow — edge push, Hibernatable WebSockets, EventSub | Cloudflare Workers, KV, Durable Objects, Hibernatable WebSockets, EventSub | +| [Cost-per-user as a design constraint](scaling-streaming-toolsets/) | Per-user real-time on Cloudflare's edge — centralized session worker, hibernatable WebSockets over per-user Durable Objects, and a cost shape that stays flat as active users grow | Cloudflare Workers, KV, Durable Objects, Hibernatable WebSockets, EventSub | | [Chat bot memory](chat-bot-memory/) | Persistent memory for a Twitch chat bot without storing raw chat logs | C#, Streamer.bot, Gemini Flash | | [Collab detection](collab-detection/) | Confidence-ranked collab detection for Twitch from several imperfect signals | Twitch Helix API, Prisma, PostgreSQL | | [How I built P.A.T.H.O.S.](how-i-built-pathos/) | Building a job-search system around deterministic scoring, constrained AI, and pipeline intelligence | React 19, Supabase, Gemini | diff --git a/scaling-streaming-toolsets/README.md b/scaling-streaming-toolsets/README.md index 418881c..fa01109 100644 --- a/scaling-streaming-toolsets/README.md +++ b/scaling-streaming-toolsets/README.md @@ -1,23 +1,39 @@ -# Scaling Per-User Streaming Toolsets on Cloudflare — Edge Push, Hibernation, and Flat Cost-Per-User +# Cost-Per-User as a Design Constraint: Per-User Real-Time on Cloudflare's Edge -**Date:** 2026-05-11 +**Date:** 2026-05-23 **Author:** deutschmark --- ## Abstract -A per-user streaming toolset on Cloudflare's edge — Workers, KV, Durable Objects, Pages — composed so cost-per-user stays roughly flat as user count grows. Each streamer runs eight OBS browser-source overlays; the hot path is push, not pull. The now-playing widget polls Spotify directly using a worker-minted short-lived token, eliminating server-mediated polling. The seven event-driven overlays subscribe to a per-user Durable Object via hibernatable WebSocket; dashboard saves and Twitch EventSub webhooks dispatch through service bindings. KV is cold persistence — read on session start and on save, never on a poll. The shape changes cost from `O(N × K × polls/hour)` to `O(N × events/hour)`, where `events ≪ polls` at any non-trivial usage. +A per-user streaming toolset on Cloudflare's edge — Workers, KV, Durable Objects, Pages — designed from day one so that the operating bill scales with active users, not with overlay count × polling frequency. Each streamer runs ~eight OBS browser-source overlays; the hot path is push, not pull. The now-playing widget polls Spotify directly using a worker-minted short-lived token. The event-driven overlays subscribe to a per-user Durable Object via hibernatable WebSocket. A centralized auth worker at `auth.deutschmark.online` issues a single signed session that every product surface — toolset, dev portal, collab planner — consumes, so no per-product worker has to reimplement OAuth, refresh, or cookie scoping. KV is cold persistence: read on session start and on save, never on a poll. The shape changes cost from `O(N × K × polls/hour)` to `O(N × events/hour)`, where `events ≪ polls` at any non-trivial usage. --- -## 1. Architecture +## 1. The constraint -Static-export Next.js on Cloudflare Pages, three Workers (`auth`, `spotify`, `overlay-do`), one KV namespace (`AUTH_KV`). The per-user product surface is eight overlays: now-playing, death counter, lurk-peek, BRB player, emote rain, clip play, video shout-out, chat box. +Per-user real-time tooling has a specific cost shape. Each user runs `K` overlays. Each overlay either polls some upstream every `P` seconds or holds an open connection waiting for events. Naively, a polling architecture costs `O(N × K × polls/hour)` requests against your edge — a curve that crosses the free-tier ceiling at one active user and the paid-tier monthly budget somewhere between ten and one hundred. That curve is not a problem you can solve by tuning poll intervals or adding cache; it's the wrong shape, and the only fix is to change the shape. -### 1.1 Layer A — Thin-client Spotify +The design constraint this paper documents: cost-per-user has to stay flat (or sublinear) as `N` grows, on a platform where every user runs the full set of overlays simultaneously, served from a free-tier-by-default Cloudflare account. That constraint shaped every architectural choice in §2 and §3 — not because anything broke at small scale, but because the curve would have made the product un-shippable at scale, and re-architecting after launch costs more than designing for the cliff from the start. -The browser polls `api.spotify.com` directly. The worker's role is reduced to minting a short-lived Spotify access token on demand. +--- + +## 2. Architecture + +Static-export Next.js on Cloudflare Pages, four Workers (`auth`, `spotify`, `overlay-do`, plus a small `toolkit-redirect` legacy-URL shim), one KV namespace (`AUTH_KV`). The per-user product surface is ~eight overlays: now-playing, death counter, lurk-peek, BRB player, emote rain, clip play, video shout-out, chat box. Several non-overlay surfaces — toolset dashboard, dev portal, collab planner — share the same session layer. + +### 2.1 `auth.deutschmark.online` — the session boundary + +A single Worker at `auth.deutschmark.online` is the only thing on the platform that talks to Twitch / Spotify / YouTube OAuth, mints sessions, refreshes tokens, and sets cookies. It issues a signed `dm_session` cookie scoped to `Domain=.deutschmark.online`, so every product subdomain (`toolset`, `dev`, `collab`, the apex) sees the same logged-in user without each shipping its own auth dance. + +The shape is deliberate: per-product workers (`spotify`, `overlay-do`) don't implement OAuth at all. They accept either the SSO cookie or a service-binding call from `auth` and trust the session claim. New product surfaces inherit auth by virtue of being on the right domain — adding `dev.deutschmark.online` was a CORS allowlist line, not a re-implementation. The same worker handles widget-token issuance (the opaque `wid` strings in overlay URLs), Stripe webhook verification, and EventSub callback HMAC. + +Centralizing this in one Worker also keeps the security boundary auditable: there's one place that knows how to read a session, one place that mints tokens, one place that talks to OAuth upstreams. Per-product workers can't accidentally accept an unsigned token because they don't have the verification key. + +### 2.2 Layer A — Thin-client Spotify + +The browser polls `api.spotify.com` directly. The `spotify` worker's role is reduced to minting a short-lived Spotify access token on demand. ``` Browser (once per ~50 min) @@ -32,9 +48,9 @@ Browser (every 10 s) └─→ Spotify API directly ``` -The Spotify upstream poll volume is unchanged; it moves from the worker's quota into the streamer's per-user OAuth quota where it belongs. Spotify's 30/min rate cap protects against runaway abuse downstream. +The Spotify upstream poll volume is unchanged; it moves from the worker's quota into the streamer's per-user OAuth quota where it belongs. Spotify's 30/min rate cap protects against runaway abuse downstream. The hot path never crosses this project's KV or worker request budget — every poll the streamer's OBS makes is paid for by Spotify's infrastructure, not Cloudflare's. -### 1.2 Layer B — Durable Object substrate with hibernatable WebSocket +### 2.3 Layer B — Durable Object substrate with hibernatable WebSocket A per-user Durable Object (`OverlayDO`, named by `twitchId`) holds in-memory state. Overlays open WebSocket connections via `state.acceptWebSocket()` — Cloudflare's Hibernation API keeps the connection live across DO eviction; the class methods `webSocketMessage` / `webSocketClose` deliver events without holding CPU. DO duration billing accrues only while CPU is active. @@ -50,133 +66,128 @@ Event sources converge on the same dispatch chain: [subscribed overlay clients] ``` -The DO broadcasts a typed event to all subscribed sockets, filtered by overlay kind. The 7 event-driven overlays are connected to this substrate; the EventSub registration covers `channel.raid`, `channel.follow`, and `channel.update` (the last drives Twitch-category autofill for the death counter). +The DO broadcasts a typed event to all subscribed sockets, filtered by overlay kind. The event-driven overlays connect to this substrate; the EventSub registration covers `channel.raid`, `channel.follow`, and `channel.update` (the last drives Twitch-category autofill for the death counter). The KV→DO move for hot state was planned from the start as the shape the system should take once it had non-trivial event traffic; an early observability alarm just provided the trigger to cut over on a calm day rather than under pressure. -### 1.3 Layer C — KV as cold persistence +### 2.4 Layer C — KV as cold persistence -After Layers A and B, KV is touched in three places only: session-start config reads, user-driven save writes, and EventSub-secret verification on webhook delivery. The hot path — every poll, every event broadcast — does not touch KV. +After Layers A and B, KV is touched in three places only: session-start config reads, user-driven save writes, and EventSub-secret verification on webhook delivery. The hot path — every poll, every event broadcast — does not touch KV. KV's role in this system is durable, infrequently-accessed configuration; not a request-path cache. -### 1.4 `wid` as the access boundary +### 2.5 `wid` as the access boundary -Overlays carry an opaque `wid` (widget token) in URL query strings; the user's `twitchId` never appears in client URLs. The DO router resolves `/by-wid//...` to `twitchId` server-side via one KV read; service-binding callers (auth, spotify worker) use the direct `/by-user//...` path with no resolution step. +Overlays carry an opaque `wid` (widget token) in URL query strings; the user's `twitchId` never appears in client URLs. The DO router resolves `/by-wid//...` to `twitchId` server-side via one KV read; service-binding callers (auth, spotify worker) use the direct `/by-user//...` path with no resolution step. The `wid` is also the unit of revocation — rotating a widget token invalidates only that overlay, not the user's session. --- -## 2. Cost analysis +## 3. Patterns -### 2.1 Cloudflare tier shape +### 3.1 Cache-layer composition -| Resource | Free | Workers Paid ($5/mo) | -|---|---|---| -| KV reads | 100k/day | 10M/month (~333k/day) | -| KV writes | 1k/day | 1M/month | -| Worker requests | 100k/day | 10M/month | -| DO requests | 100k/day | 1M/month + $0.15/M | -| DO duration | 13k GB-s/day | 400k GB-s/month + $12.50/M | +Workers expose three caching surfaces with distinct scopes: -Workers Paid is a single umbrella: KV, Durable Objects, R2, and Workers requests are billed against one $5/mo subscription. Architectures that treat these as separate upgrades waste design budget on a fiction. +| Surface | Scope | Latency | Use | +|---|---|---|---| +| In-isolate `Map` | One isolate | ~0 ms | Per-isolate dedup of identical concurrent requests | +| `caches.default` | One data center, all isolates | ~1 ms | Per-DC response cache for identical URLs | +| KV with `cacheTtl` | All edge data centers | ~5 ms | Cross-DC cache for identical keys | -### 2.2 Now-playing path (measured) +A response that crosses isolate boundaries without a `caches.default` wrap pays the full origin cost on every cross-isolate hop. The fix is small — wrap the response, set `Cache-Control`, write through `ctx.waitUntil(cache.put(...))` — and removes a class of cost invisible in development (single isolate) but real in production. -Steady-state, single user, four-hour streaming session: +### 3.2 Visibility gating for any polling client -| Metric | Naive polling | Thin-client | Δ | -|---|---|---|---| -| KV reads | ~7,200 | ~20 | −99.7% | -| Worker requests | ~1,440 | ~5 | −99.65% | -| KV reads per poll | 5 | 0 | path eliminated | -| Hidden-tab 24h burn (KV reads) | ~43,200 | 0 | structurally eliminated | -| Spotify API calls per overlay-hour | ~360 | ~360 | unchanged (moved to user's OAuth quota) | +A polling client without a `document.visibilityState` gate is a latent runaway. The cost-when-it-fires is roughly the day-budget burned by one forgotten browser tab. The fix is ~10 lines of React per polling site. There is no excuse to ship a poll loop without it. -Naive baseline is 10-second polling without an edge cache. The hidden-tab elimination is the combination of two changes: a `document.visibilityState` gate on the poll loop and the structural shift that the runaway-tab path (5 KV reads × 360 polls/hour × 24 hours = 43,200) now lives entirely client-side against Spotify, not against this project's KV. +The same logic does not transfer cleanly to WebSocket clients: they don't poll when hidden, but they do reconnect on transient network errors. A flapping WebSocket with exponential backoff topping at 30 s on a hidden tab costs ~2,880 KV reads/day per overlay if each reconnect resolves `wid → twitchId` against KV. The mitigation is in §5. -### 2.3 Event-driven paths (projected) +### 3.3 Push over polling for event-shaped data -The seven event-driven overlays exchanged their polling endpoints for WebSocket subscriptions. Per-event cost on the post-migration architecture, accounted at the call-site level: +For data that changes on discrete events (chat commands, webhook deliveries, user actions), polling is structurally wrong. Cloudflare's Hibernation API for Durable Objects is the supported path: WebSocket connections persist across DO eviction, in-memory state hydrates lazily on first event, duration billing accrues only during active CPU. Cost shape goes from `O(polls)` to `O(events)`. -- EventSub callback verifies a per-subscription secret: 1 KV read. -- DO routing resolves `wid → twitchId` on each WebSocket connect/reconnect: 1 KV read. -- `channel.update` fan-out across `M` death-counter widget tokens: 1 (index) + M (per-record) KV reads. -- Each `config-changed` broadcast triggers an overlay refetch: 1 KV read per connected overlay. +### 3.4 Centralized auth as a platform discipline -So a single `!death`-style chat event costs `1 + M + K·M` KV reads where `M` is widget-token count and `K` is connected overlays per token. This is non-zero but bounded by event frequency, not polling frequency. A 4-hour streaming session with 50 chat events and one overlay per token costs ~150 KV reads on the event-driven path — versus the naive-polling projection below. +Every product subdomain on `.deutschmark.online` shares one signed session, one cookie, one OAuth upstream contract. The discipline is platform-level: per-product workers can read the session claim but cannot mint one. New surfaces inherit auth by being on the right domain. -| Users | Naive (worker req/day) | Architecture as deployed | -|---|---|---| -| 1 | 138,240 | ~200 | -| 10 | 1.38M | ~2,000 | -| 100 | 13.8M | ~20,000 | -| 1,000 | 138M | ~200,000 | -| 10,000 | 1.38B | ~2M | +This is the inverse of the common "every app has its own auth" pattern. It costs slightly more cognitive load upfront — there's one Worker to maintain that owns a lot — but it removes per-product auth-bug surface, lets the security boundary be reviewed in one place, and means a Stripe webhook arriving for a user signed in on the toolset can identify them on the collab planner without any cross-app session shuttle. -Naive baseline: 8 overlays × 5-second polling × 4-hour session. Per-architecture-as-deployed estimates assume 50 chat events per session and the call-site KV cost above. These are projections, not measurements — see §4 for the architectural changes that would tighten them further. +### 3.5 Static-export feature flags + +Next.js bakes `NEXT_PUBLIC_*` env vars into the static bundle at build time. With `output: "export"` on Cloudflare Pages, dashboard env-var changes do nothing until the next build; non-prefixed runtime env vars are unavailable to client-side code (no Node process at request time). + +The implication: a static-export Pages app has no live runtime feature flags. Every "flag" is either build-time (rebuild to flip) or moves into a config endpoint (which itself becomes a polling source). Flag-then-delete on a build-time literal is a defensible idiom for migration rollout; flag-as-permanent is anti-pattern on this stack. --- -## 3. Patterns +## 4. Cost shape -### 3.1 Cache-layer composition +### 4.1 Cloudflare tier shape -Workers expose three caching surfaces with distinct scopes: +| Resource | Free | Workers Paid ($5/mo) | +|---|---|---| +| KV reads | 100k/day | 10M/month (~333k/day) | +| KV writes | 1k/day | 1M/month | +| Worker requests | 100k/day | 10M/month | +| DO requests | 100k/day | 1M/month + $0.15/M | +| DO duration | 13k GB-s/day | 400k GB-s/month + $12.50/M | -| Surface | Scope | Latency | Use | -|---|---|---|---| -| In-isolate `Map` | One isolate | ~0 ms | Per-isolate dedup of identical concurrent requests | -| `caches.default` | One data center, all isolates | ~1 ms | Per-DC response cache for identical URLs | -| KV with `cacheTtl` | All edge data centers | ~5 ms | Cross-DC cache for identical keys | +Workers Paid is a single umbrella: KV, Durable Objects, R2, and Workers requests bill against one $5/mo subscription. Architectures that treat these as separate upgrades waste design budget on a fiction. -A response that crosses isolate boundaries without a `caches.default` wrap pays the full origin cost on every cross-isolate hop. The fix is small — wrap the response, set `Cache-Control`, write through `ctx.waitUntil(cache.put(...))` — and removes a class of cost invisible in development (single isolate) but real in production. +### 4.2 What this design actually costs to operate -### 3.2 Visibility gating for any polling client +Per-user, four-hour streaming session, all overlays connected. KV reads per session sum to roughly: -A polling client without a `document.visibilityState` gate is a latent runaway. The cost-when-it-fires is roughly the day-budget burned by one forgotten browser tab. The fix is ~10 lines of React per polling site. There is no excuse to ship a poll loop without it. +- 1 read for session-start auth (the SSO cookie's session claim resolves via one KV lookup) +- 1 read per overlay token resolution on connect (`wid → twitchId`), so ~K per session +- 1 read per Spotify token refresh (~5 per session, once per ~50 min) +- 1 read per webhook callback (EventSub HMAC secret verification — a couple per session) +- 1 read per overlay refetch triggered by a config-changed broadcast (rare; dashboard-driven) -The same logic does not transfer cleanly to WebSocket clients: they don't poll when hidden, but they do reconnect on transient network errors. A flapping WebSocket with exponential backoff topping at 30 s on a hidden tab costs ~2,880 KV reads/day per overlay if each reconnect resolves `wid → twitchId` against KV. The mitigation is in §4. +That sums to ~50 KV reads per user per session for the typical case, with the long tail driven by event volume (chat commands, EventSub callbacks). Worker requests follow the same shape — a handful per session, scaling with events rather than polls. -### 3.3 Push over polling for event-shaped data +### 4.3 Projection to user count -For data that changes on discrete events (chat commands, webhook deliveries, user actions), polling is structurally wrong. Cloudflare's Hibernation API for Durable Objects is the supported path: WebSocket connections persist across DO eviction, in-memory state hydrates lazily on first event, duration billing accrues only during active CPU. Cost shape goes from `O(polls)` to `O(events)`. +Per-user-per-day usage, all 8 overlays connected, 50 chat events per session, projected against the Workers Paid monthly budget: -### 3.4 Static-export feature flags +| Active users | KV reads/day | Worker req/day | Headroom on $5/mo plan | +|---|---|---|---| +| 1 | ~50 | ~200 | 99.99% | +| 10 | ~500 | ~2,000 | 99.93% | +| 100 | ~5,000 | ~20,000 | 99.34% | +| 1,000 | ~50,000 | ~200,000 | 93% | +| 10,000 | ~500,000 | ~2M | 34% | -Next.js bakes `NEXT_PUBLIC_*` env vars into the static bundle at build time. With `output: "export"` on Cloudflare Pages, dashboard env-var changes do nothing until the next build; non-prefixed runtime env vars are unavailable to client-side code (no Node process at request time). +These are call-site estimates from the architecture in §2, not measurements. The shape is right; the absolute numbers will move with event mix. The Workers Paid tier (one $5/mo subscription covering Workers, KV, DO, and R2) absorbs through ~10,000 active streamers before DO duration or KV read overage become the binding constraint — and the §5 changes push the binding constraint further. -The implication: a static-export Pages app has no live runtime feature flags. Every "flag" is either build-time (rebuild to flip) or moves into a config endpoint (which itself becomes a polling source). Flag-then-delete on a build-time literal is a defensible idiom for migration rollout; flag-as-permanent is anti-pattern on this stack. +For context, a polling-style architecture at the same user count and overlay count crosses the *daily* free-tier ceiling at one active user and the Workers Paid *monthly* budget somewhere between ten and one hundred. The shape isn't a tuning problem; it's the difference between costing flat and costing quadratic. --- -## 4. Known optimization paths +## 5. Known optimization paths -The architecture-as-deployed in §1 is not the lower bound. Three changes would meaningfully reduce the event-driven KV cost of §2.3: +The architecture in §2 is not the lower bound. Three changes would meaningfully reduce the event-driven KV cost: -- **Signed `wid → twitchId` claim.** The DO router currently resolves `wid` to `twitchId` via a KV read on every WebSocket connect/reconnect. A signed (HMAC) claim baked into the wid at issuance eliminates that read entirely. WebSocket reconnect storms drop to zero KV cost. Largest single hidden-cost source on the event path. -- **DO SQLite for widget config.** Widget configuration currently lives in KV under `widget_token:`; every `config-changed` broadcast triggers `K` overlays to refetch via KV. Moving config into the DO's SQLite storage and including the new state inside the broadcast payload eliminates the post-event refetch storm. The `K·M` factor in §2.3's accounting collapses to zero. +- **Signed `wid → twitchId` claim.** The DO router currently resolves `wid` to `twitchId` via a KV read on every WebSocket connect/reconnect. A signed (HMAC) claim baked into the `wid` at issuance eliminates that read entirely. WebSocket reconnect storms drop to zero KV cost. Largest single hidden-cost source on the event path; the auth worker already mints and verifies enough signed material to make this a small extension rather than a new system. +- **DO SQLite for widget config.** Widget configuration currently lives in KV under `widget_token:`; every `config-changed` broadcast triggers connected overlays to refetch via KV. Moving config into the DO's SQLite storage and including the new state inside the broadcast payload eliminates the post-event refetch storm. - **Coalesced broadcast payload.** The current `config-changed` event carries a `key` field; overlays then refetch. Embedding the full new state in the event removes the refetch round-trip entirely. Pairs naturally with the SQLite-storage change above. ### Surviving polling -Two dashboard-side polls remain after the overlay migration: `useSupportPool` (60 s, fetches the community-fund total) and the chat-bot health check (5 s against `localhost`). The first is candidate-for-SSE; the second is local-loopback only and does not consume Cloudflare quota. +Two dashboard-side polls remain after the overlay migration: `useSupportPool` (60 s, fetches the community-fund total) and the local-loopback chat-bot health check (5 s against `localhost`). The first is candidate-for-SSE; the second does not consume Cloudflare quota. --- -## 5. Cost projection by user count +## 6. Cost model and funding shape -Assumptions: 4-hour streaming session per user per day, all 8 overlays connected, 50 chat events per session. +Cloudflare cost is not the only operating cost. The full per-month operating expense (DNS, Workers Paid, Pages, occasional R2, Stripe processing) sits at a known floor `F`, and grows roughly linearly with active users `N` once usage clears the free-tier shoulders: -| Users | Naive polling (worker req/day) | Architecture-as-deployed | With §4 optimizations applied | -|---|---|---|---| -| 1 | 138,240 | ~200 | ~50 | -| 10 | 1.38M | ~2,000 | ~500 | -| 100 | 13.8M | ~20,000 | ~5,000 | -| 1,000 | 138M | ~200,000 | ~50,000 | -| 10,000 | 1.38B | ~2M | ~500k | +``` +T(N) = F + c · N +``` -Naive crosses the free-tier daily ceiling at one user, the paid-tier daily-equivalent at ten. Architecture-as-deployed stays in free-tier headroom through 1,000 active streamers; with §4 applied, through 10,000. +Concretely on this platform: `F ≈ $6` and `c ≈ $0.03` per active user per month, derived from the §4 usage shape and the Workers-Paid overage rates. That cost model is the input to the platform's `/support` community-fund target — the funding ask scales with active user count rather than being a fixed number that drifts out of relevance — and recomputes monthly so the goal tracks reality. The design discipline this enforces is symmetric to the technical one: the bill is allowed to grow, but it has to grow in proportion to something users can see (themselves), not as a surprise. --- -## 6. Conclusion +## 7. Conclusion -The system is push, not pull. Polling on the hot path is replaced with a worker-minted Spotify token (the browser polls the upstream directly) and a per-user Durable Object holding hibernatable WebSockets (event sources dispatch through service bindings; the DO broadcasts to subscribed overlays). KV is cold persistence — read on session start, written on user-driven save events. +The system is push, not pull. Polling on the hot path is replaced with a worker-minted Spotify token (the browser polls the upstream directly) and a per-user Durable Object holding hibernatable WebSockets (event sources dispatch through service bindings; the DO broadcasts to subscribed overlays). KV is cold persistence — read on session start, written on user-driven save events. A centralized auth Worker at `auth.deutschmark.online` issues one signed session that every product surface consumes, so per-product workers don't reimplement OAuth. -The now-playing path is measured; the event-driven path is projected at the call-site level, with three named architectural changes in §4 that would tighten those projections further. For per-user real-time tools on Cloudflare's edge, the patterns documented here — cache-layer composition, visibility gating on poll loops, push for event-shaped data, signed claims over KV lookups on the connect path — compose to a system that scales with user count, not with overlay count × polling frequency. +The cost shape that result composes — flat in poll frequency, linear in event volume, sublinear in per-user fixed cost amortized across the platform — is the design constraint, not a happy accident. It was chosen from the start because the polling alternative would have made the product un-shippable past the free-tier shoulders, and re-architecting after launch costs more than designing for the cliff at the start.