Case studies / Open source

Cutting Gumroad Sales API response time in half

An open source Rails performance fix that removed N+1 queries from Gumroad’s sales endpoint while preserving its API response.

Gumroad Sales API results at a glance: 51% faster requests, 83% less SQL time, 74% fewer SQL events, 94% fewer association point lookups, and 31% fewer allocations, with the same API output.

At a glance

ProblemRepeated database queries while serializing a page of sales
ScopeGET /api/v2/sales in Gumroad’s Rails application
Result50.9% lower mean request duration and 74.2% fewer SQL events
DeliveryTested open source change merged upstream

The problem

Gumroad’s Sales API was already preloading some data, but serializing each Purchase touched associations outside that preload set. A page of ten fully populated sales repeatedly queried buyers, products, variants, custom fields, followers, upsells, shipments, and reviews.

Two less obvious query sources compounded the problem: follower status was looked up once per sale, and variant serialization created a fresh scoped relation even when the variants had already been loaded.

What I changed

I profiled the endpoint to identify the queries triggered during serialization, then moved that work from per-record lookups to page-level loading:

  • preloaded the associations traversed by the sale serializer, including nested upsell data;
  • batch-loaded variant categories and active follower emails;
  • passed precomputed follower status into Purchase#as_json;
  • reused loaded variant records instead of creating another relation; and
  • applied the optimized path to both page-key and deprecated page-number pagination.

The fallback behavior in Purchase#as_json remained intact for callers outside the Sales API. Case-insensitive follower matching was also preserved.

Measured result

I benchmarked five requests per revision after three warmups, using a page of ten sales populated with distinct products, variants, custom fields, followers, shipments, reviews, and upsells.

MetricBefore meanAfter meanChange
Request duration284.66 ms139.70 ms−50.9%
SQL duration76.31 ms13.21 ms−82.7%
Allocated objects82,38556,504−31.4%
SQL events15540−74.2%
Duplicate query fingerprints13316−88.0%
Association point lookups1358−94.1%

Validation

The regression coverage used distinct products so Active Record’s association cache could not conceal per-product queries. It exercised populated nested associations, both pagination modes, and mixed-case follower emails. The focused controller suite passed 102 examples, and the full upstream CI suite was green before merge.

The endpoint’s JSON contract was unchanged; the improvement came from doing the same work in batches.

View the merged Gumroad pull request →