In the era of content consumption where streaming platforms like YouTube and Netflix have millions of concurrent users, no one wants to wait more than a minute to consume anything, it must be available instantly!

Streaming becomes a need in this situation. While downloading requires waiting until the entire file has been downloaded on your computer, streaming allows you to view downloaded portions of the material on the fly. This significantly reduces the waiting time.

Let's see with the help of a small example how streaming was handled before Rails 7 and what changed after Rails 7.

With Rails < 7

Suppose we need to write a controller action that streams data chunks to the client in real time as they are getting downloaded.

Before Rails 7, we were required to include the ActionController::Live module for live streaming to work. By mixin the ActionController::Live module in our application's controller, we enable all actions in that controller to stream data to the client as it is written in real-time.

Note: The response headers must be explicitly specified in all actions of the controller. This enables the browser to recognize the type of file it should display.

class VideoController < ApplicationController
  include ActionController::Live

  def show
    response.headers["Content-Type"] = @video.content_type
    response.headers["Content-Disposition"] = "inline; #{@video.filename.parameters}"

    # Streaming downloaded chunks of data to the client as the video downloads
    @video.download do |chunk|
      response.stream.write(chunk)
    end

    response.stream.close
  end
end

Content-Disposition is a response header indicating if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved locally.

Few caveats with Rails < 7:

  1. You need to write headers for the actions before a response has been committed (calling write or close on the response stream will cause the response object to be committed). Make sure all headers are set before calling write or close on your stream.
  2. You must close the stream when it's finished, otherwise, the socket will remain open forever.

With Rails >= 7

ActiveStorage::Streaming module is introduced in this PR, which supports native file streaming from the controller. Once we include ActiveStorage::Streaming in a controller, we get access to the #send_blob_stream method which takes care of everything, from writing the headers to streaming the downloaded data chunks to the client to closing the stream after it is completed.

Now we don't need to re-write response headers and close the stream before and after every streaming action.

class VideoController < ApplicationController
  include ActiveStorage::Streaming

  def show
    http_cache_forever(public: true) do
      send_blob_stream @video, disposition: params[:disposition]
    end
  end
end

The method http_cache_forever allows us to set response headers to tell the browser that the response has not been modified. It means that on the first hit, we serve the request normally but, then on each subsequent request, the cache is revalidated and a 304 Not Modified response is sent to the browser. This saves us from setting the same response header on every request.

# First request:

Processing by VideoController#show as HTML
  Rendered videos/show.html.erb within layouts/application (1.8ms)
Completed 200 OK in 198ms (Views: 256.7ms | ActiveRecord: 0.0ms)

# Consecutive requests for the same page:

Processing by VideoController#show as HTML
Completed 304 Not Modified in 3ms (ActiveRecord: 0.0ms)

Rails has started to expand its streaming capabilities, and we may see more updates in the coming months. Checkout most recent updates in the pull requests below:

  1. Extract ActiveStorage::Streaming so your own controllers can use it
  2. Don't stream redirect controller responses
  3. Rails ActiveStorage::Streaming module

References: