Run Solid Queue in async mode on Render free tier#23
Merged
Conversation
Companion to #22 (WEB_CONCURRENCY=0). Free tier (512MB) can't fit SQ's default fork mode, which spawns supervisor + worker + dispatcher + scheduler subprocesses for ~460MB of RSS overhead on top of Puma. The container OOM-cycles every ~7-15 min — the deploy still looks "live" but HTTP returns 502s during each restart window. `solid_queue_mode :async` (documented switch in lib/puma/plugin/solid_queue.rb of the gem) tells the plugin to run worker/dispatcher/scheduler as threads inside the Puma master process. The 4 SQ subprocesses collapse into ~50MB of thread overhead in Puma. Container RSS drops from ~530MB (cycling into OOM) to a flat ~300MB. Trade-off per SQ's own docs ("Only use async if you know what you're doing and have strong reasons to"): - Less isolation between SQ and Puma. A leaky/hung SQ thread affects request serving. For low-traffic student projects this is the right call; revisit if upgrading to a paid plan. - No inter-process parallelism for jobs. Free tier is already 0.1 vCPU, so this isn't real lost throughput. - Recurring tasks and concurrency controls still work unchanged. The DB pool bump (5 → 8) is non-optional: previously the pool was sized for Puma's 3 request threads only; now the same process also runs 3 SQ worker threads + dispatcher + scheduler. Under any load the old pool=5 would throw ConnectionTimeoutError. The 5 -> 8 default still respects DB_POOL env override for projects that need finer control. Diagnosed and validated on raghubetina/aplace: raghubetina/aplace#58 — RSS metric showed the predicted plateau at ~300MB with zero growth after deploy.
This branch's puma.rb adds `solid_queue_mode :async if ENV["SOLID_QUEUE_IN_PUMA"]`, but the lockfile resolved to solid_queue 1.1.4, which has no such DSL method. Booting in production crashes immediately with: config/puma.rb:47:in 'Puma::DSL#_load_from': undefined method 'solid_queue_mode' for an instance of Puma::DSL (NoMethodError) Async supervisor mode was re-introduced in solid_queue 1.3.0 (rails/solid_queue#644); the earlier 0.4 implementation was removed in 0.7. Pinning to ~> 1.3 documents the minimum that matches this branch's puma.rb and updates the lockfile to 1.4.0. Caught by deploying a marketplace smoke-test app built from this branch + #22 + #24 to Render free tier.
bpurinton
reviewed
May 28, 2026
bpurinton
left a comment
Contributor
There was a problem hiding this comment.
LGTM, made the proj syncing update too: https://github.com/firstdraft/project-syncing/commit/dc66431569160a23b64aefaf8793665931d32eb1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Companion to #22 (`WEB_CONCURRENCY=0`). Switches SQ to async mode so it fits in the 512MB Render free plan, plus the matching DB pool bump (5 → 8) so the in-process SQ threads don't starve Puma's request threads for connections.
Why this is needed
Render free is 512MB. With SQ's default fork mode, the supervisor spawns 4 subprocesses (~460MB across them) on top of Puma. The container OOM-cycles every ~7-15 min; deploys look "live" but HTTP returns 502s during each restart window.
`solid_queue_mode :async` is a documented switch in the SQ Puma plugin (`lib/puma/plugin/solid_queue.rb`) that runs worker/dispatcher/scheduler as threads inside Puma master. The 4 subprocesses collapse to ~50MB of thread overhead.
Trade-offs (per SQ docs)
SQ explicitly says "Only use async if you know what you're doing and have strong reasons to." The strong reasons here:
The downsides — less isolation between SQ and Puma, a hung SQ thread affecting request serving — are acceptable for portfolio projects with effectively zero traffic. The comment in `config/puma.rb` notes to revisit on a paid plan.
Pool change
The pool bump is non-optional in async mode. Previously sized for Puma's 3 request threads only; now the same process also runs 3 SQ worker threads + dispatcher + scheduler. `pool: 5` was right at the edge; under any load it would throw `ActiveRecord::ConnectionTimeoutError`. New default 8 respects the existing `DB_POOL` env override for finer control.
Validation
Diagnosed and shipped on raghubetina/aplace (PR #58). Render's RSS metric showed the predicted plateau at ~300 MB with zero growth after deploy — confirmed via that app's memory_logger and Render's metric API.
Sequencing note
This PR is independent of #22 — they don't conflict. Order of merge doesn't matter. A separate follow-up will collapse the production multi-DB role config (`primary`/`cache`/`queue`/`cable`) to a single role since student projects don't benefit from the pool isolation.
Test plan