Performance First Rails – Lessons From Production

| Shey Sewani | Toronto

Performance First Rails – Lessons From Production

This post is a work in progress. I’ll keep expanding and refining it over time.

Most Rails performance problems aren’t about Ruby. They’re about data: how it’s accessed, serialized, cached, and moved through your system.

These are practices I’ve picked up from running Rails in production. Some are obvious, others less so.


🗃️ Data First, Always

Your database is the bottleneck long before Ruby is.

  • Avoid N+1 queries. Yes, still.
  • Use exists? instead of count > 0.
  • Use pluck to avoid loading columns you don’t need.
  • Never call .all. Use pagination library or chunking with .in_batches instead.
  • For massive tables, consider partitioning especially for time-based data.

Indexing

  • Composite indexes should reflect your actual query filters and sort order.
  • Use partial indexes to address query skew and workload imbalances. If a single tenant generates the majority of reads on a table, create a targeted index for just their slice of data. This avoids full-table indexes and keeps read performance fast for high-traffic accounts.
  • Too many indexes will slow down inserts and updates—index only what you read often.
  • Monitor slow queries and experiment with different indexes.

🟥 Redis

Redis gets abused and forgotten.

For caching Redis, reduce how often data is written to disk. Optimize save and appendonly settings. If you overload a single Redis instance, you’ll experience weird latency spikes.

  • Split workloads:
    • One Redis for jobs (Sidekiq)
    • One for caching (Rails.cache)
    • One for ephemeral features (rate limiting, feature flags, etc.)

🦵 Sidekiq Queues & Concurrency

  • More queues = more polling, more complexity, more starvation risk.
  • Sidekiq latency problems are often queue starvation or Redis slowness, not job logic.
  • Tune concurrency to match job intensity. CPU-bound work needs fewer threads.

💾 Cache Aggressively

The less work your database has to do, the faster everything else gets.

  • Use HTTP caching where applicable.
  • Cache HTML fragments for common UI blocks.
  • Cache expensive queries and API responses.
  • Use jsonapi-serializer for fast, structured JSON. It’s composable and cache-aware.
  • Make sure you have TTL entries on your cache items.

🌊 Pool Sizes & Throughput

Puma, Sidekiq, Postgres, Redis—they all have pool settings. If they’re misaligned, you’ll see weird timeouts and throughput drops.

  • Configure Postgresql and Redis’ connection pool size to RAILS_MAX_THREADS + 1.
  • Higher thread and worker counts can overwhelm your database. Experiment.
  • Monitor wait times in your connection pools. They’re early indicators of saturation.

🚀 Ruby & Gems

Ruby performance matters, but it’s not the first lever.

Before tuning code, audit your gems:

  • Avoid gems that wrap everything in callbacks or monkey patches.
  • Avoid gems with inefficient data access patterns—they don’t scale.

🛰️ Still to Come

  • Memory pressure and tuning for large multi-tenant workloads.
  • Strategies for load testing with vegeta.
  • Notes on replicas, analytics traffic, and write isolation.
  • Horizontal scaling basics: how and why to split workloads across differently configured hosts.