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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Streaming responses with ActionController::Live let you send data as it's produced instead of building the whole payload in memory.
- 2Pairing find_each with a live stream keeps both database and response memory bounded no matter how many rows you export.
- 3A live stream must always be closed in ensure, and client disconnects should be caught rather than crash the action.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
ruby
class ApplicationController < ActionController::Base EXPERIMENTS = { checkout_button_color: %w[control blue green], onboarding_flow: %w[control streamlined]
How A/B test cohorts are assigned in Rails
a-b-testing
cookies
hashing
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-a-csv-export-in-rails-explained-ruby-6565/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.