Skip to content

ADR-0020: SSE Feed — D1 Polling over In-Memory Pool

Context

The notification feed for the TV display (issue #146) needs to deliver notifications in real time. Two competing approaches:

  1. In-memory pool: Worker maintains Map<churchId, WritableStream[]>; POST creates notification and pushes to active streams.
  2. D1 polling: SSE endpoint does SELECT on D1 every 3s and emits new notifications as SSE events.

Decision

Use D1 polling (approach 2).

Consequences

Positive

  • Works with multiple Worker instances — Cloudflare scales horizontally without losing notifications
  • Survives deploysEventSource reconnects and polling resumes from lastCheck
  • Simpler to implement and test (no pool management)
  • No risk of memory leak from orphaned connections

Negative

  • ~3s latency between creation and display on the TV screen (vs real-time with pool)
  • D1 query cost: 1 query every 3s per active SSE connection (~20/min)
  • CPU time: polling is I/O-bound (D1 wait), not CPU-bound — does not impact 30s limit

Mitigations

  • 3s latency is acceptable for church TV display (the parent is already being called in-person)
  • D1 query is indexed by (church_id, created_at) and returns at most ~1 row per polling cycle
  • If latency becomes a problem, can migrate to pool without breaking clients (EventSource is compatible)

Alternatives Considered

AlternativeReason for rejection
In-memory poolLoses connections on deploy; doesn't scale with multiple Worker instances
WebSocket with Durable ObjectsHeavier infrastructure dependency; over-engineered for MVP
D1 + Durable Object as brokerUnnecessary complexity for expected notification volume

References

Distributed under MIT License.