Cloudflare Ports

proto-poker and proto-retro were built on Vercel with Upstash Redis. These ports rebuild both on Cloudflare, as an experiment in how the two platforms differ. The specific question was the Vercel versions' dependence on Upstash Redis and the limits of its free database.

The rule for the port was to change the platform and nothing else: same product, same frontend, same public HTTP API. That makes the differences that remain the result. The repos are private (eswat2/proto-poker-cf, eswat2/proto-retro-cf).

The limits being tested

On Vercel, state and realtime both depend on a second vendor, Upstash, added through the Vercel Marketplace. Its free plan set the shape of the system:

Normal use stayed far below these limits: a reading on 2026-09-08 showed 6.4K commands and 5KB of storage. The concern was never the everyday load. It was that one misbehaving client or a third app could use up a quota two working apps shared, and that proto-draw could burn a full day's usage in one session.

What changed

Concern Vercel + Upstash Cloudflare
State JSON blob at a Redis key One Durable Object per room or board, with its own SQLite-backed storage
Concurrent writes Lua compare-and-swap, jittered retry up to 10 times None needed: a Durable Object handles one event at a time
Fan-out Redis PUBLISH, one SUBSCRIBE connection per SSE stream The writer and every reader are in the same object
Realtime SSE, reconnecting at least every 5 minutes Hibernatable WebSockets by default, SSE as a fallback
Expiry Redis key TTL, refreshed on write Durable Object alarms: 1h idle and empty, 2h idle, 4h lifetime
Rate limits Redis INCR and EXPIRE Counters in the room, plus one Limiter Durable Object per client IP
Quota Shared between both apps Each app has its own Durable Object namespaces
Tests Needs a local Redis running Vitest inside a local workerd, with nothing to start first

The design was anticipated. proto-poker's original decision log named an actor or Durable-Object-style owner as the alternative to compare-and-swap, and rejected it for Vercel because it would have meant building a queue and consumer on top. On Cloudflare that model is the platform's default, so the retry logic went away instead of being replaced. The test that fires 25 concurrent writers was kept to prove nothing is lost.

What it found

The vendor dependency goes away, but the free-plan limit doesn't. State, coordination and realtime are now all one platform, with no shared database and no second console. There's still a ceiling. It just moves, and it's a different kind of limit: metered time and rows written instead of commands.

The transport decides the cost. A direct SSE port would have been the smallest change, but an open SSE stream keeps a Durable Object resident for as long as a tab is open, at about 460 GB-s per room-hour. That made duration the first free-plan limit, at an estimated 27 one-hour sessions a day. Hibernatable WebSockets cost duration only while a message is being handled, so that limit effectively disappears and rows written becomes the ceiling, at an estimated 90 sessions a day. This is why WebSockets were added to a port meant to change nothing else.

SSE stayed anyway. Some corporate proxies block WebSocket upgrades but pass a plain streaming response, and curl -N can watch a room. The cost is per room: one SSE client keeps that room resident, while rooms with only WebSockets still hibernate.

Hibernation shapes the code. A pending timer keeps a Durable Object awake, so there are no WebSocket timers in the room. Disconnects rely on the runtime's close handling, and keepalive pings are answered without waking the object.

Some things got more hand-built. Workers have one entrypoint, so routing is a small hand-written router instead of one file per route, and per-IP rate limiting needed its own Durable Object instead of a Redis key.

eswat2.app

Both ports run on one Cloudflare domain, which is itself a small comparison point. Each app is its own Worker on a subdomain, poker.eswat2.app and retro.eswat2.app, and the root is a static page with no Worker code. Wrangler creates the DNS records and certificates, and pushes to main deploy through Workers Builds.

Linking the live apps is safe here in a way it isn't for the Vercel versions, whose URLs proto-realtime leaves out to protect the free tier. Creating a room or board needs a creator key, joining takes only a link, and static pages never touch a Durable Object, so casual crawler traffic doesn't reach the limits.

The session estimates and GB-s figure come from the ports' architecture decision logs, and the Upstash figures from the Vercel versions' project notes. Both are as measured at the time and may not match current vendor plans.