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
module DeepImmutable module_function def deep_freeze(obj)
Deep-freezing config with recursive immutability
immutability
recursion
deep-copy
Intermediate
8 steps
ruby
class DatasetImportJob < ApplicationJob queue_as :default def perform(import)
Streaming import progress with a Rails job
background-jobs
turbo-streams
progress-tracking
Intermediate
9 steps
ruby
class Booking < ApplicationRecord belongs_to :room belongs_to :guest
Guarding booking dates in a Rails model
validations
associations
scopes
Intermediate
5 steps
ruby
class DashboardMetrics CACHE_TTL = 15.minutes def initialize(account)
Caching dashboard metrics in Rails
caching
service-object
query-optimization
Intermediate
5 steps
ruby
class LRUCache def initialize(capacity) raise ArgumentError, "capacity must be positive" unless capacity.positive?
How an LRU cache works in Ruby
caching
eviction
hash-ordering
Intermediate
7 steps
ruby
class QueryParser def self.parse(query_string) new(query_string).parse end
Parsing nested query strings in Ruby
parsing
recursion
tokenization
Intermediate
9 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.