From 2565efb0f5ed4d32f2b1c077eeb85aa0fc9249a6 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 18 May 2026 10:08:41 -0500 Subject: [PATCH 1/2] Run Solid Queue in async mode on Render free tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/raghubetina/aplace/pull/58 — RSS metric showed the predicted plateau at ~300MB with zero growth after deploy. --- config/database.yml | 7 ++++++- config/puma.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/config/database.yml b/config/database.yml index 5bb10df..d6ae37f 100644 --- a/config/database.yml +++ b/config/database.yml @@ -17,7 +17,12 @@ default: &default encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: <%= ENV.fetch("DB_POOL") { 5 } %> + # + # Sized for Puma's 3 request threads + Solid Queue's async-mode threads + # (3 worker + dispatcher + scheduler) sharing this pool. The previous + # default of 5 was right at the edge and produced ConnectionTimeoutError + # under any load once SQ threads moved into the Puma process. + pool: <%= ENV.fetch("DB_POOL") { 8 } %> development: diff --git a/config/puma.rb b/config/puma.rb index a248513..2978d42 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -36,6 +36,16 @@ # Run the Solid Queue supervisor inside of Puma for single-server deployments plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] +# Run Solid Queue in async mode: worker/dispatcher/scheduler run as threads +# inside the Puma master process instead of being forked into 4 subprocesses. +# Required on Render's 512MB free plan — fork mode adds ~460MB of process +# overhead (supervisor + 3 children) and pushes the container into OOM-loop. +# Async mode collapses that to ~50MB of additional thread overhead in Puma +# master. Trade-off: less isolation between SQ threads and Puma — a hung or +# leaky SQ thread affects request serving. For low-traffic student projects +# the trade-off is right; revisit if upgrading to a paid plan with more RAM. +solid_queue_mode :async if ENV["SOLID_QUEUE_IN_PUMA"] + # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. pidfile ENV["PIDFILE"] if ENV["PIDFILE"] From ee8fc7ac30e8d8ada58882f01f1012fe6a7f778b Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 18 May 2026 11:50:07 -0500 Subject: [PATCH 2/2] Pin solid_queue ~> 1.3 for solid_queue_mode :async support 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. --- Gemfile | 2 +- Gemfile.lock | 70 ++++++++++++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Gemfile b/Gemfile index 22ea4bd..d794bff 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem "importmap-rails" # JavaScript with ESM import maps gem "turbo-rails" # Hotwire page acceleration (SPA-like) gem "stimulus-rails" # Hotwire JavaScript framework gem "solid_cache" # Database-backed Rails.cache -gem "solid_queue" # Database-backed Active Job +gem "solid_queue", "~> 1.3" # Database-backed Active Job — 1.3+ required for solid_queue_mode :async in config/puma.rb gem "solid_cable" # Database-backed Action Cable gem "bootsnap", require: false # Faster boot times via caching gem "thruster", require: false # HTTP caching/compression for Puma diff --git a/Gemfile.lock b/Gemfile.lock index 34b08c6..67a1852 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,7 +86,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (4.1.0) + bigdecimal (4.1.2) bindex (0.8.1) binding_of_caller (2.0.0) debug_inspector (>= 1.2.0) @@ -131,8 +131,9 @@ GEM devise indefinite_article drb (2.2.3) + erb (6.0.4) erubi (1.13.1) - et-orbi (1.2.11) + et-orbi (1.4.0) tzinfo faraday (2.12.2) faraday-net_http (>= 2.0, < 3.5) @@ -154,10 +155,10 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake - fugit (1.11.1) - et-orbi (~> 1, >= 1.2.11) + fugit (1.12.1) + et-orbi (~> 1.4) raabro (~> 1.4) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) grade_runner (0.0.16) activesupport (>= 2.3.5) @@ -188,8 +189,9 @@ GEM indefinite_article (0.2.5) activesupport io-console (0.8.2) - irb (1.15.1) + irb (1.18.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.19.3) @@ -211,7 +213,7 @@ GEM ffi-compiler (~> 1.0) rake (~> 13.0) logger (1.7.0) - loofah (2.24.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -223,8 +225,7 @@ GEM matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) - mini_portile2 (2.8.9) - minitest (6.0.2) + minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) msgpack (1.8.0) @@ -240,24 +241,21 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.4) - mini_portile2 (~> 2.8.2) + nokogiri (1.19.3-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.4-aarch64-linux-gnu) + nokogiri (1.19.3-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.4-aarch64-linux-musl) + nokogiri (1.19.3-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.4-arm-linux-gnu) + nokogiri (1.19.3-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.4-arm-linux-musl) + nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.4-arm64-darwin) + nokogiri (1.19.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.4-x86_64-darwin) + nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.4-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.4-x86_64-linux-musl) + nokogiri (1.19.3-x86_64-linux-musl) racc (~> 1.4) octokit (5.6.1) faraday (>= 1, < 3) @@ -293,13 +291,13 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.5) - rack-session (2.1.0) + rack (3.2.6) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) - rackup (2.2.1) + rackup (2.3.1) rack (>= 3) rails (8.0.2) actioncable (= 8.0.2) @@ -319,12 +317,12 @@ GEM actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails_db (2.5.0) activerecord @@ -343,15 +341,17 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) ransack (4.3.0) activerecord (>= 6.1.5) activesupport (>= 6.1.5) i18n - rdoc (6.12.0) + rdoc (7.2.0) + erb psych (>= 4.0.0) + tsort regexp_parser (2.11.3) - reline (0.6.0) + reline (0.6.3) io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) @@ -438,13 +438,13 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_queue (1.1.4) + solid_queue (1.4.0) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) - fugit (~> 1.11.0) + fugit (~> 1.11) railties (>= 7.1) - thor (~> 1.3.1) + thor (>= 1.3.1) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) @@ -453,7 +453,7 @@ GEM unicode-display_width (~> 3.0) terminal-table (4.0.0) unicode-display_width (>= 1.1.1, < 4) - thor (1.3.2) + thor (1.5.0) thruster (0.1.12) thruster (0.1.12-aarch64-linux) thruster (0.1.12-arm64-darwin) @@ -490,7 +490,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.8.0) zip (2.0.2) PLATFORMS @@ -541,7 +541,7 @@ DEPENDENCIES shoulda-matchers (~> 7.0) solid_cable solid_cache - solid_queue + solid_queue (~> 1.3) stimulus-rails thruster tsort