Skip to content

Server-Sent Events Support#220

Merged
rsamoilov merged 24 commits intomainfrom
sse
Mar 12, 2026
Merged

Server-Sent Events Support#220
rsamoilov merged 24 commits intomainfrom
sse

Conversation

@rsamoilov
Copy link
Member

@rsamoilov rsamoilov commented Feb 25, 2026

Native Server-Sent Events (SSE) Support

This PR adds native support for server-sent events, enabling real-time, server-to-client streaming over HTTP with a simple, idiomatic API.

Why SSE?

Server-Sent Events provide a lightweight alternative to WebSockets when you only need server-to-client communication - think live notifications, activity feeds, progress updates, or streaming AI responses. Unlike WebSockets, SSE works over standard HTTP, plays nicely with existing infrastructure (load balancers, proxies, CDNs), and automatically handles reconnection out of the box.

Rage now makes SSE a first-class citizen with three approaches depending on your use case:

Streams

Use Enumerator objects to stream events as they're generated. This is ideal for sequential data or when the server controls the pace:

render sse: "Hello, world!".each_char

For more control, create a custom enumerator - useful for long-running computations, database cursors, or AI token streaming:

stream = Enumerator.new do |y|
  "Hello, world!".each_char do |ch|
    sleep 1
    y << ch
  end
end

render sse: stream

Pub/Sub

The pub/sub functionality will be delivered in the next PR.

When events originate from background jobs, other services, or different parts of your application, use Rage::SSE::Stream to decouple the connection from the event source:

render sse: Rage::SSE::Stream.new("user-notifications-#{current_user.id}")

Then broadcast from anywhere:

Rage::SSE.broadcast("user-notifications-#{user.id}", "Hello, world!")

One-off Updates

For simple cases where you just need to push a single payload over SSE (e.g., compatibility with SSE-only clients):

render sse: "Hello, world!"

Rich Message Support

Use Rage::SSE.message to include SSE metadata like event types, IDs for resumption, and retry intervals:

stream = Enumerator.new do |y|
  y << Rage::SSE.message(user.profile, id: user.id, event: "profile_loaded")
end

render sse: stream

Low-level Access

For advanced use cases requiring full control over the connection lifecycle and wire format, pass a proc:

render sse: ->(connection) do
  connection.write("data: Hello, world!\n\n")
ensure
  connection.close
end

In this mode, Rage hands you the raw connection - you're responsible for formatting and cleanup.

Depends on rage-rb/iodine#9.

fixes the case when fiber is manually killed while waiting
@rsamoilov rsamoilov mentioned this pull request Feb 25, 2026
Copy link

@megatux megatux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks great.
I have some questions. Regarding the pub/sub stuff or the rage-iodine gem update, are they strictly part of this SSE support or are things that can be part of separated PRs?
Also, I guess we need spec tests for this new controller functionality

@rsamoilov
Copy link
Member Author

Thanks - really appreciate you taking the time to look through this 🙏

A bit of context: this PR is more of a foundation step than a release candidate. I've been designing SSE support for a while, and what's here is the initial end-to-end integration, but it's not production-ready yet. I expect it will need several more weeks of hardening (performance testing, edge cases, etc.) before I'm comfortable cutting a release.

To answer your questions regarding the scope:

  • rage-iodine update: This is strictly necessary for this PR. Because Rage and the server operate as a unified runtime, this update contains the core server-level changes and stability fixes required to actually make SSE work under the hood.
  • Pub/sub functionality: You're right, this isn't strictly required for basic SSE support. I'm happy to strip this out of the current branch to reduce the scope and move it to a separate PR later.
  • Spec tests: Absolutely, we definitely need them.

If you want to experiment early, I'd recommend bundling directly from GitHub for now. Once I'm confident in stability and performance, I'll cut a proper release.

I'm trying to be very deliberate with new primitives like this - once they're public API, they're very hard to change. So I'd rather take the time now than rush something half-baked.

Feedback on the design is very welcome though.

@rsamoilov rsamoilov merged commit 4f3bad5 into main Mar 12, 2026
12 checks passed
@rsamoilov rsamoilov deleted the sse branch March 12, 2026 13:35
@rsamoilov
Copy link
Member Author

Hi @megatux

This has been merged and release in v1.22.0!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants