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 even nil results 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 .map for .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.nowTime.zone.now) with the exact Rubocop command to apply it.
  • Improved their tooling. Docker for the local database, dotenv-rails to sync config with Heroku, Rubocop support, and getting their background jobs visible in their APM.

Facing a similar problem? Let's talk about it.

Contact Me