At a glance
| Problem | Repeated database queries while serializing a page of sales |
| Scope | GET /api/v2/sales in Gumroad’s Rails application |
| Result | 50.9% lower mean request duration and 74.2% fewer SQL events |
| Delivery | Tested 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.
| Metric | Before mean | After mean | Change |
|---|---|---|---|
| Request duration | 284.66 ms | 139.70 ms | −50.9% |
| SQL duration | 76.31 ms | 13.21 ms | −82.7% |
| Allocated objects | 82,385 | 56,504 | −31.4% |
| SQL events | 155 | 40 | −74.2% |
| Duplicate query fingerprints | 133 | 16 | −88.0% |
| Association point lookups | 135 | 8 | −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.