Case Study
Fixing Rails performance on a live charity SaaS
Memory errors, a thrashing cache, and N+1 queries on a Heroku app failing at seven concurrent users — diagnosed and fixed in the client's own codebase.
This one was advisory in posture and hands-on in practice. I diagnosed the problems, then opened pull requests against their production app and the CTO merged them.
The company
A UK social-impact SaaS — outcomes and impact measurement for charities and social enterprises. A Rails monolith on Heroku, around 100 concurrent users and climbing, with a codebase that, in the CEO's own words, had "been written by many developers" with a mix of quality. They came in through the contact form: about 100 concurrent users, real performance problems, and not enough in-house expertise to solve them.
What I did
The technical brief was stark — over 2,000 request-timeout and memory errors in seven days, and the app falling over at seven or more concurrent users. So I went after the memory problem first, because nothing else matters while your app servers keep dying. The R14 errors traced to doing too much work in memory, plus a suspected thread-spawning leak in one controller.
From there:
- Reworked the caching layer. Some requests were making tens of thousands of Redis calls each. I moved them toward Rails' built-in
redis_cache_store, added a local cache, fixed the cache-miss handling so evennilresults got stored, moved to an LRU strategy with versioned keys, and split the Redis instances used for caching from the ones used for background jobs. - Rewrote hot-path queries. Swapped
.mapfor.pluck, replaced a per-record permission scan with a proper scope, and flagged the worst offenders — one endpoint allocating millions of objects per request. - Audited schema and data integrity. A long itemized list: missing foreign keys, redundant indexes, missing null constraints and the validations to match, missing unique indexes behind uniqueness checks, and the timezone fix (
Time.now→Time.zone.now) with the exact Rubocop command to apply it. - Improved their tooling. Docker for the local database,
dotenv-railsto sync config with Heroku, Rubocop support, and getting their background jobs visible in their APM.