ruby 41 lines · 8 steps

Streaming a CSV export in Rails

How ActionController::Live streams a large CSV to the client row-by-row without buffering it all in memory.

Explained by highlit
1class Admin::OrdersExportController < Admin::BaseController
2 include ActionController::Live
3 
4 def show
5 response.headers["Content-Type"] = "text/csv"
6 response.headers["Content-Disposition"] = %(attachment; filename="orders-#{Date.current.iso8601}.csv")
7 response.headers["Last-Modified"] = Time.current.httpdate
8 response.headers["X-Accel-Buffering"] = "no"
9 
10 writer = CSV.new(response.stream)
11 writer << %w[id placed_at customer_email status subtotal tax total currency]
12 
13 scope
14 .includes(:customer)
15 .find_each(batch_size: 500) do |order|
16 writer << [
17 order.id,
18 order.placed_at&.iso8601,
19 order.customer.email,
20 order.status,
21 order.subtotal_cents / 100.0,
22 order.tax_cents / 100.0,
23 order.total_cents / 100.0,
24 order.currency,
25 ]
26 end
27 rescue ActionController::Live::ClientDisconnected
28 logger.info("Orders export aborted: client disconnected")
29 ensure
30 response.stream.close
31 end
32 
33 private
34 
35 def scope
36 orders = current_account.orders.order(:id)
37 orders = orders.where(status: params[:status]) if params[:status].present?
38 orders = orders.where(placed_at: params[:from]..params[:to]) if params[:from].present?
39 orders
40 end
41end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Streaming responses with ActionController::Live let you send data as it's produced instead of building the whole payload in memory.
  2. 2Pairing find_each with a live stream keeps both database and response memory bounded no matter how many rows you export.
  3. 3A live stream must always be closed in ensure, and client disconnects should be caught rather than crash the action.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a CSV export in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code