Skip to content

Cache the rendered /sponsors page body — recurring 1.3s render stalls dyno threads #2885

Description

@mroderick

Summary

/sponsors re-renders 721 sponsor fragments on every request, even when the per-sponsor fragments are cache hits. That costs ~1.3s of CPU-bound Ruby per render (~123 renders in a 3h window). On production dynos (puma, 1 worker × 3 threads) a CPU-bound render holds the GVL and stalls unrelated requests on the same dyno. And after a deploy or dyno restart the in-process fragment cache is empty, so the first render takes 17–21s.

Proposal: cache the rendered page body in the shared cache store (Solid Cache), keyed on the latest sponsors.updated_at, so a warm request costs a single cache read.

Measured

From production canonical logs (3h window, 123 requests) and a local reproduction against a production dump:

Metric Value
median view_runtime (warm, fragments all hits) 1,337ms
allocations per render ~2.5M (17.7M worst)
db_runtime / queries 41ms / 5 queries
cold render after dyno restart 17.8s local repro, 21.3s in production
page size ~210KB HTML, 721 logos

Local reproduction: cold 17.8s, warm ~1.1s — matching production's max and median.

Why #2803 didn't fix it

#2803 fragment-cached each sponsor partial (cached: true) and removed the CarrierWave allocation hot spot. But the per-request cost is now dominated by the collection machinery itself: building 721 fragment keys, reading 721 fragments, and assembling a 210KB page — all CPU-bound Ruby work, per render. It also lives in a per-process MemoryStore (ActionView's own collection_cache), which is wiped by every restart.

Proposed fix

Cache the whole rendered body, keyed on the most recent sponsor change:

def index
  key = "sponsors/index/#{Sponsor.active.maximum(:updated_at)&.to_fs(:usec)}"
  body = Rails.cache.fetch(key) { render_to_string }
  render html: body.html_safe
end

Properties:

  • Solid Cache is DB-backed, so the body survives dyno restarts and deploys — no more 17s cold renders
  • Any sponsor edit bumps updated_at → new key → one re-render, then warm again
  • Stale window: none (key changes the moment a sponsor is saved)

Expected impact

Median /sponsors view drops from ~1,337ms to ~10–20ms (one cache read + one render-to-string every sponsor edit), and the recurring GVL stall disappears from both dynos.

Related: #2883, #2803

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    performancePerformance issues and optimizations

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions