Rack 3 introduced support for input and output streaming. This is a powerful feature that improves both performance and provides opportunities for enhanced interactivity in web applications. We will explore how to use streaming input and output in Rack, including server-sent events, streaming templates, streaming uploads, and bi-directional streaming. We will also discuss how to use these features in Ruby on Rails.
All the source code discussed in this article is available on GitHub. I recommend you check it out so you can follow along with the examples and try it out for yourself.
Streaming Output
Streaming output is a feature that allows you to send data to the client as it is generated. This is useful for sending large amounts of data, such as video or audio streams, or for sending data that is generated over time, such as a log file or a real-time feed. Streaming output is supported in Rack by returning a callable object as the response body. This callable object will get invoked with a stream argument, which you can read and write to from within the block.
The following example shows how to emit the current date and time in CSV format:
You can use the curl
command to test the streaming output:
Use cases for streaming CSV output include:
- Large Report Exports
- Users can download extensive datasets as they're being processed, reducing wait times and enhancing user experience.
- Financial Data Feeds
- Deliver real-time transaction or portfolio data in CSV format for continuous access by analysts and clients.
- Log Monitoring
- Stream application or server logs in CSV format for immediate analysis and rapid response to issues.
- Real-Time Business Intelligence
- Stream CSV data from sources like CRMs or IoT devices for up-to-date business intelligence and reporting.
Streaming Server-Sent Events (SSE)
Streaming output can be a bit tricky to use in web browsers, as they may buffer the response. One way to work around this is to use Server-Sent Events (SSE), which are specifically handled by web browsers and are guaranteed to be unbuffered.
The following example shows how to use streaming output with SSE:
Of note, is the error handling. It's important to be aware of the various failure cases that can occur when streaming data. In the above example, we stream data indefinitely. If there is a failure for some reason, we will close the stream with the error. Closing the stream with an error will communicate to the client that the stream was terminated prematurely.
On the client side, you'll need to use the EventSource interface to receive the events. Here's an example of how you might do that:
Event streams are easy to use - they are supported by all modern browsers and handle reconnections and other edge cases automatically. This makes them a great choice for streaming data to the client.
Use cases for streaming SSE include:
- Live Notifications
- Push real-time notifications for events like promotions, user activity updates, or order status changes to keep users informed instantly.
- Real-Time Dashboards
- Stream continuous updates to data visualization dashboards for monitoring systems, sales metrics, or analytics without manual refreshes.
- Live Polls and Q&A
- Stream responses and results for interactive polls, Q&A sessions, or audience participation during live events and webinars.
- Chat and Messaging Apps
- Deliver new messages instantly as they arrive, creating a more responsive chat experience for users.
Streaming Templates
Most web frameworks use templates to generate HTML. These templates are typically rendered in full before being sent to the client. However, in some cases it's possible to stream the template as it is being generated. This greatly improves the time to first byte, at the expense of slightly more complex error handling. By allowing the server to send chunks from the template as they are generated, the client can start rendering the page sooner. This is particularly useful for large pages, or pages with complex logic that takes time to render.
Here is an example of a template with sleep statements to simulate a slow rendering page:
The following example shows how to use streaming templates with Falcon:
You can also do this with ERB templates, but you need to use a streaming-compatible renderer. Rails does not fully support this right now, but we are working on it!
Streaming Input
Streaming input is a feature that allows you to read data from the client as it is received. This is useful for processing large amounts of data, such as file uploads, or for processing data that is generated over time, such as a real-time feed. Streaming input is supported in Rack by reading from the input stream provided by the request environment.
The following example shows how to read a file upload from the client and process it chunk by chunk to generate a chuecksum:
If you upload a file (even a large one), it will be sent in chunks, and the checksum will be computed without buffering the entire input:
Bi-directional Streaming
Full bi-directional streaming is a useful feature for building interactive applications. By combining streaming input and streaming output, you can read and write data to the client in real-time.
The following example shows how to implement a simple chat server using bi-directional streaming:
Very few clients (e.g. curl) support bi-directional streaming, so we implemented a client to go with the example:
Due to the limitations of web browser implementations, bi-directional streaming is not well-supported in modern interfaces like fetch. However, it is still useful for scenarios that don't involve a browser, or for wrapping higher level protocols:
- Real-Time Transcription & Translation
- Stream audio data to a server for real-time transcription and translation, and stream the results back to the client for immediate feedback.
- Efficient HTTP RPC
- Some RPC protocols, like gRPC, can be implemented over HTTP using bi-directional streaming for efficient communication between clients and servers.
- WebSockets
- In Rack, WebSockets can and are implemented using bi-directional streaming.
Bi-directional WebSockets
To work around the limitations of bi-directional streaming in web browsers, you can use WebSockets. WebSockets provide a full-duplex communication channel over an HTTP connection. This reduces the difficulty of implementing real-time communication in web browsers.
The following example shows how to use WebSockets with Falcon:
On the client side, you can use the WebSocket API to send and receive messages. Here's an example of how you might do that:
Use cases for WebSockets include:
- Progressive Enhancement
- Enhance traditional web applications with incremental updates and real-time features for a more interactive user experience.
- Live Collaboration
- Support real-time editing and collaboration on documents, spreadsheets, or codebases for remote teams.
- Interactive Games
- Facilitate real-time multiplayer gaming experiences with features like chat, leaderboards, and live updates.
What about Rails?
Rails builds on top of Rack, so most of the examples above work unchanged, provided you use self.response = Rack::Response[...]
in the controller action, for example, the server-sent events example can be done like this:
Of course, for the best performance, you should be running on top of Falcon. You can also check out falcon-rails-example for the above examples (and more) adapted to Rails. You might also like to check out my previous article on interactive Rails applications.
RubyConf AU 2023 Presentation
In this talk, I discuss asynchronous execution models in Ruby, and how we can build streaming applications using Rack and Falcon. I discuss some of the above approaches in the context of Ruby on Rails.
You can find the slides for my presentation here.
Conclusion
Rack 3 introduces powerful streaming features that improve performance and provide opportunities for enhanced interactivity in web applications. By leveraging streaming input and output, you can build applications that are more responsive and efficient, delivering real-time experiences that were previously challenging to achieve. By making full-duplex streaming easy to use in Rack and Falcon, we aim to position Ruby and Rails as an exceptional choice for building modern, interactive applications that require high performance and real-time capabilities.