S2 as stream backend: cost analysis, architecture options, protocol mapping, CDN collapsing, implementation reference#43
S2 as stream backend: cost analysis, architecture options, protocol mapping, CDN collapsing, implementation reference#43scottmessinger with Copilot wants to merge 12 commits into
Conversation
Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
durable-streams-demo-draw | 0f4f75f | Commit Preview URL Branch Preview URL |
Feb 17 2026, 06:32 PM |
Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
|
@copilot You missed the memo here. It seems like you're trying to make a port instead of solving the goals more creatively. There are a couple goals:
I'm interested in s2 because it's not our DB and it's not DO. I'm interested in object storage. It's possible using slatedb (lower level) is fine, too. Having one instance is a non-starter. This must scale to several instances. Please rethink this working from my goals and first principles. |
…mory state Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
…hutdown, docs clarity Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Rewrote the design from scratch in 053d8f8. The previous version was a port — GenServer-per-stream, in-memory state, single-instance s2-lite. The new version works from your goals: Stateless Elixir app. No GenServers, no in-memory state. The app is a thin HTTP layer (~250-500 lines) that proxies to S2. Any instance can handle any request. S2 does the heavy lifting. Writes go directly to S2 (object storage). SSE connections each open an S2 No thundering herd. Redeploying Elixir nodes loses nothing because there's nothing in memory. Clients reconnect to any instance with their Multi-instance from day one. Multiple Elixir replicas behind a load balancer. No distributed Erlang, no coordination. S2 (managed) handles horizontal scaling of the storage layer. Two fan-out options. Option B (recommended): clients read source streams directly — S2 handles read fan-out internally. Option A: session stream copies for when you need per-subscriber filtering. Managed S2 for production. s2-lite is for dev/test only. Managed S2 scales horizontally with no code changes. |
|
@copilot if we're using s2 for so much, why not hit it directly? Also, I don't think the s2's economics are awesome for this use case. Please point why we need elixir and what it's doing. Also, find if there are ways to convert s2 to durable stream's protocol. |
…a read fan-out Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
…eader count thresholds Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Rewrote in 8ad616c to directly address all three questions. Why not hit S2 directly? You can — it's Option 1 in the doc. Works for server-to-server. Doesn't work for browser clients (no per-stream auth, no CORS, S2 tokens in client code). For those you need at minimum a thin adapter (Option 2, ~300 lines). S2 economics. Did the math with your pricing screenshots. ReadSession at $0.000001/min means 10K SSE readers = $432/mo — worse than Cloudflare's $18/mo. This is the exact same problem as DO duration billing (Chapter 2). The fix is the same too: read fan-out. One S2 session per active stream → broadcast to N clients → cost becomes O(streams) not O(readers). That's Option 3, and it's the only reason a middleware layer is justified at scale. s2-lite changes the equation entirely. Self-hosted = no per-call pricing. Option 2 (thin adapter) works at any reader count because read sessions are free. The only cost is the K8s pod (~$30/mo). This may be the pragmatic path for your K8s deployment. Protocol mapping. Full Durable Streams ↔ S2 translation table is in the doc. The mapping is clean: |
|
@copilot I believe s2 has per stream auth (https://s2.dev/docs/api/protocol). Please update the docs after having read these. Also, if there's a protocol mapping, can we then use the CDN collapsing from durable streams (and as done in this library with the read keys for protection)? |
Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Updated in 3638654. S2 per-stream auth: Corrected — S2 does have per-stream auth. The S2 natively serves SSE: Also discovered that S2's read endpoint returns CDN collapsing with DS protocol: Yes — added as Option 4. If the adapter translates S2 responses to DS format with Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot I want to support both a lot of SSE client and long polling. It's not either or -- it's both. What combines both (which the current CF DO solution handles) |
Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
…fix section link Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Restructured in 78593b5. Option 4 now handles both SSE and long-poll at scale — same dual-mode approach as the current CF implementation:
The adapter is ~500 lines (auth + CORS + protocol translation + SSE fan-out + long-poll handler). The SSE fan-out is the only stateful part — it holds a per-stream S2 session + client list + ring buffer. On redeploy, SSE clients auto-reconnect with |
|
@copilot Great. now, create a new file that simply explores " Option 4 now handles both SSE and long-poll at scale". i want to see files that would be created, sample code, etc. Diagram it all out. Cut the fluff (the whys, the timeline, etc), and just focus on what it would look like. |
…code Co-authored-by: scottmessinger <100121+scottmessinger@users.noreply.github.com>
Created 13 source files with complete sample code, no prose:
|


Evaluation of S2 (managed or s2-lite) as the storage layer replacing Cloudflare Durable Objects + SQLite + R2. Analyzes four architecture options against actual S2 pricing, with a detailed Durable Streams ↔ S2 protocol mapping and a complete implementation reference for the recommended option.
docs/14-elixir-s2-implementation.mdS2 Auth Model
AccessTokenScope.streams(exact match or prefix)op_groups.streamS2 Native SSE
text/event-streamwhenAccept: text/event-streamis setbatch(records),error,ping(keepalive)S2 Cost Analysis
Four Architecture Options
Durable Streams ↔ S2 Protocol Mapping
Stream-Cursorgeneration for CDN collapsing, producer fencingdocs/15-s2-adapter-implementation.md(NEW)Complete implementation reference for Option 4 — no prose, just code and diagrams:
/v1/stream/:basin/*stream_path)Cache-Control+Stream-Cursorfor CDN collapsing), HEAD, deletedocs/00-index.mdUpdated with entries for both Chapter 14 and Chapter 15.
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.