From 129678571f4eff536b4ab0b825ea2aabcb9ec382 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 29 Dec 2025 18:29:39 -0600 Subject: [PATCH 1/5] Optimize memory usage for Render deployment Configure Puma to run in single mode by default to save memory. Set MALLOC_ARENA_MAX to 2 to reduce fragmentation. Enable Solid Queue in Puma to avoid extra worker processes. --- config/puma.rb | 9 +++++ perf_1.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ render.yaml | 4 +++ 3 files changed, 102 insertions(+) create mode 100644 perf_1.md diff --git a/config/puma.rb b/config/puma.rb index a248513..806793c 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -27,6 +27,15 @@ threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +web_concurrency = ENV.fetch("WEB_CONCURRENCY", 0).to_i +workers web_concurrency if web_concurrency > 1 + # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) diff --git a/perf_1.md b/perf_1.md new file mode 100644 index 0000000..e748a32 --- /dev/null +++ b/perf_1.md @@ -0,0 +1,89 @@ +# Performance and Memory Audit Recommendations + +The following recommendations are designed to reduce memory usage for your Rails 8 application on Render.com, specifically targeting the "Free" plan limits. + +## 1. Reduce Puma Workers (`WEB_CONCURRENCY`) + +**Issue:** The current configuration in `render.yaml` sets `WEB_CONCURRENCY` to `2`. +```yaml +- key: WEB_CONCURRENCY + value: 2 +``` +This forces Puma to run in "Clustered Mode" with 2 worker processes. Each worker forks the application, effectively doubling the memory footprint required for the Rails application code. On a memory-constrained environment (like the free plan), this often leads to Out-Of-Memory (OOM) kills. + +**Recommendation:** +Set `WEB_CONCURRENCY` to `1` (or effectively 0). This runs Puma in "Single Mode" (threads only). While this limits theoretical maximum throughput on multi-core systems, it drastically reduces memory usage, which is the bottleneck here. + +**Action:** +Update `render.yaml`: +```yaml +- key: WEB_CONCURRENCY + value: 1 +``` + +## 2. Tune Memory Allocator (`MALLOC_ARENA_MAX`) + +**Issue:** The default glibc memory allocator can create fragmentation in multi-threaded Ruby applications, causing "bloat" where memory is reserved but not used. + +**Recommendation:** +Set the `MALLOC_ARENA_MAX` environment variable to `2`. This is a standard optimization for Ruby apps to trade a tiny bit of performance for significantly tighter memory usage. + +**Action:** +Add to `render.yaml` environment variables: +```yaml +- key: MALLOC_ARENA_MAX + value: 2 +``` + +## 3. Run Solid Queue in Puma + +**Issue:** Rails 8 uses Solid Queue for background jobs. You need a way to process these jobs. Running a separate "worker" service costs money and memory. Running a separate process inside the web service also consumes more RAM. + +**Recommendation:** +Use the `solid_queue` Puma plugin to run job processing threads *inside* the web process. This shares the memory of the Rails app between web requests and background jobs. + +**Action:** +1. Ensure your `config/puma.rb` has this line (it currently does): + ```ruby + plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + ``` +2. Enable it in `render.yaml`: + ```yaml + - key: SOLID_QUEUE_IN_PUMA + value: true + ``` + +## 4. Puma Configuration Adjustment + +**Issue:** The `config/puma.rb` script sets `workers` based on `WEB_CONCURRENCY` if the variable exists. If you set `WEB_CONCURRENCY` to 1, some configurations might still attempt to use cluster mode (workers = 1), which has higher overhead than single mode (workers = 0). + +**Recommendation:** +Update `config/puma.rb` to explicitly only enable workers if the count is greater than 1. + +**Action:** +Update `config/puma.rb`: +```ruby +# ... +web_concurrency = ENV.fetch("WEB_CONCURRENCY", 0).to_i +workers web_concurrency if web_concurrency > 1 +# ... +``` + +## Summary of `render.yaml` Changes + +```yaml +services: + - type: web + # ... + envVars: + - key: SECRET_KEY_BASE + generateValue: true + - key: DATABASE_URL + sync: false + - key: WEB_CONCURRENCY + value: 1 + - key: MALLOC_ARENA_MAX + value: 2 + - key: SOLID_QUEUE_IN_PUMA + value: true +``` diff --git a/render.yaml b/render.yaml index ce95a7e..1f4fda4 100644 --- a/render.yaml +++ b/render.yaml @@ -11,4 +11,8 @@ services: - key: DATABASE_URL sync: false - key: WEB_CONCURRENCY + value: 1 + - key: MALLOC_ARENA_MAX value: 2 + - key: SOLID_QUEUE_IN_PUMA + value: true From f0cee318043c5b147080f4ac1e2029e9a5cb618f Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 29 Dec 2025 18:56:40 -0600 Subject: [PATCH 2/5] Apply comprehensive memory optimizations Includes recommendations from perf_4.md and previous analyses: - Consolidate DB production pools (perf_2.md, perf_4.md) - Force Puma single mode via WEB_CONCURRENCY=1 (perf_1.md, perf_4.md) - Enable Solid Queue in Puma for durable jobs (perf_1.md, perf_4.md) - Tune MALLOC_ARENA_MAX=2 (perf_1.md, perf_4.md) - Move dev/test gems out of default group (perf_3.md) - Robust ENV handling in Puma and boot scripts --- Gemfile | 13 +- bin/render-start.sh | 2 +- config/database.yml | 3 - config/environments/production.rb | 2 +- config/initializers/appdev_support.rb | 14 +- config/puma.rb | 3 +- perf_2.md | 245 ++++++++++++++++++++++++++ perf_3.md | 55 ++++++ perf_4.md | 108 ++++++++++++ render.yaml | 13 ++ 10 files changed, 441 insertions(+), 17 deletions(-) create mode 100644 perf_2.md create mode 100644 perf_3.md create mode 100644 perf_4.md diff --git a/Gemfile b/Gemfile index f1b3ac5..2cd209f 100644 --- a/Gemfile +++ b/Gemfile @@ -59,14 +59,9 @@ end # Additional gems for AppDev gem "active_link_to" gem "ai-chat" -gem "appdev_support" -gem "awesome_print" gem "devise" -gem "dotenv" gem "carrierwave" gem "cloudinary" -gem "faker" -gem "htmlbeautifier" gem "http" gem "kaminari" gem "pagy" @@ -77,6 +72,14 @@ gem "simple_form" gem "strip_attributes" gem "validate_url" +group :development, :test do + gem "appdev_support" + gem "awesome_print" + gem "dotenv" + gem "faker" + gem "htmlbeautifier" +end + group :development do gem "annotaterb" gem "better_errors" diff --git a/bin/render-start.sh b/bin/render-start.sh index e954e5e..b4e68fa 100644 --- a/bin/render-start.sh +++ b/bin/render-start.sh @@ -3,4 +3,4 @@ set -o errexit # Ruby on Rails -bundle exec rails server +exec bundle exec rails server -e "${RAILS_ENV:-production}" -b 0.0.0.0 -p "${PORT:-3000}" diff --git a/config/database.yml b/config/database.yml index a5f20c9..3d0b8cb 100644 --- a/config/database.yml +++ b/config/database.yml @@ -85,13 +85,10 @@ production: encoding: utf8 cache: <<: *primary_production - database: database_production_cache migrations_paths: db/cache_migrate queue: <<: *primary_production - database: database_production_queue migrations_paths: db/queue_migrate cable: <<: *primary_production - database: database_production_cable migrations_paths: db/cable_migrate diff --git a/config/environments/production.rb b/config/environments/production.rb index 417324b..83901e8 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -50,7 +50,7 @@ config.cache_store = :solid_cache_store # Replace the default in-process and non-durable queuing backend for Active Job. - config.active_job.queue_adapter = :solid_queue + config.active_job.queue_adapter = ENV.fetch("RAILS_QUEUE_ADAPTER", "solid_queue").to_sym # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. diff --git a/config/initializers/appdev_support.rb b/config/initializers/appdev_support.rb index e2773f9..6e5e11b 100644 --- a/config/initializers/appdev_support.rb +++ b/config/initializers/appdev_support.rb @@ -1,7 +1,9 @@ -AppdevSupport.config do |config| - config.action_dispatch = true - config.active_record = true - config.pryrc = :minimal -end +if defined?(AppdevSupport) && (Rails.env.development? || Rails.env.test?) + AppdevSupport.config do |config| + config.action_dispatch = true + config.active_record = true + config.pryrc = :minimal + end -AppdevSupport.init + AppdevSupport.init +end diff --git a/config/puma.rb b/config/puma.rb index 806793c..c62bafa 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -43,7 +43,8 @@ plugin :tmp_restart # Run the Solid Queue supervisor inside of Puma for single-server deployments -plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] +solid_queue_in_puma = ENV.fetch("SOLID_QUEUE_IN_PUMA", "").strip.downcase +plugin :solid_queue if %w[1 true yes].include?(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. diff --git a/perf_2.md b/perf_2.md new file mode 100644 index 0000000..278890c --- /dev/null +++ b/perf_2.md @@ -0,0 +1,245 @@ +# Memory Optimization Recommendations for Render.com Deployment + +This document outlines recommendations for reducing memory usage on Render.com's free tier (512MB RAM). + +--- + +## Critical Issues + +### 1. WEB_CONCURRENCY=2 on Free Tier + +Render's free tier has ~512MB RAM. Running 2 Puma workers is too aggressive for this constraint. + +**Current:** `render.yaml` sets `WEB_CONCURRENCY: 2` + +**Recommendation:** Change to `WEB_CONCURRENCY: 1`: + +```yaml +- key: WEB_CONCURRENCY + value: 1 +``` + +**Expected impact:** ~40-50% memory reduction + +--- + +### 2. Four Separate PostgreSQL Databases + +The `config/database.yml` configures 4 separate databases in production: +- `primary` - main application data +- `cache` - for Solid Cache +- `queue` - for Solid Queue +- `cable` - for Solid Cable + +Each database connection pool consumes significant memory. With pool size 5, that's potentially 20 connections total. + +**Recommendation:** Consolidate to a single database. The Solid* gems can share the primary PostgreSQL database using different tables. + +> **Note:** SQLite is not an option on Render's free tier because it uses an ephemeral filesystem. Any SQLite databases would be wiped on every deploy or restart, losing cached data, pending background jobs, and WebSocket state. + +**Option A: Share the primary PostgreSQL database (recommended)** + +Remove the separate `database:` overrides so all use the same database but different tables: + +```yaml +# config/database.yml +production: + primary: &primary_production + <<: *default + url: <%= ENV["DATABASE_URL"] %> + cache: + <<: *primary_production + migrations_paths: db/cache_migrate + queue: + <<: *primary_production + migrations_paths: db/queue_migrate + cable: + <<: *primary_production + migrations_paths: db/cable_migrate +``` + +**Option B: Disable Solid* features entirely** + +If students don't need background jobs or WebSockets, disable these features: + +```ruby +# config/environments/production.rb + +# Use async adapter instead of Solid Queue (jobs run in-process) +config.active_job.queue_adapter = :async + +# Use in-memory cache instead of Solid Cache (simpler, lower memory) +config.cache_store = :memory_store, { size: 16.megabytes } +``` + +Then remove the `cache`, `queue`, and `cable` database entries entirely. + +**Expected impact:** ~15-20% memory reduction + +--- + +### 3. Missing Puma Workers Configuration + +The `config/puma.rb` doesn't explicitly set workers - it relies on `WEB_CONCURRENCY` but doesn't call the `workers` method. This may cause unexpected behavior. + +**Recommendation:** Add to `config/puma.rb`: + +```ruby +workers ENV.fetch("WEB_CONCURRENCY", 0) +preload_app! +``` + +The `preload_app!` directive enables Copy-on-Write memory sharing between workers, which is essential when running multiple workers. + +--- + +## High Impact Recommendations + +### 4. Add Ruby GC Tuning Environment Variables + +Add these to `render.yaml`: + +```yaml +- key: MALLOC_ARENA_MAX + value: 2 +- key: RUBY_GC_HEAP_GROWTH_FACTOR + value: 1.1 +``` + +`MALLOC_ARENA_MAX=2` is critical for reducing memory fragmentation in glibc's malloc implementation. This is one of the most effective memory optimizations for Ruby on Linux. + +**Expected impact:** ~20-30% memory reduction + +--- + +### 5. Reduce Thread Count + +The default thread count is 3. Reducing to 2 saves memory while maintaining reasonable concurrency. + +**Recommendation:** Add to `render.yaml`: + +```yaml +- key: RAILS_MAX_THREADS + value: 2 +``` + +The `database.yml` already uses `RAILS_MAX_THREADS` for pool size, so this will automatically align. + +**Expected impact:** ~10% memory reduction + +--- + +### 6. Consider jemalloc + +jemalloc is an alternative memory allocator that significantly reduces Ruby memory usage and fragmentation. + +**Option A:** Use the jemalloc gem (simpler): + +```ruby +# Gemfile +gem 'jemalloc' +``` + +**Option B:** Install jemalloc system-wide in your build script. + +**Expected impact:** ~10-20% memory reduction + +--- + +## Moderate Impact Recommendations + +### 7. Review Heavy Gems + +Some gems in the Gemfile add notable memory overhead: + +| Gem | Concern | Recommendation | +|-----|---------|----------------| +| `ransack` | Heavy query builder, loads many dependencies | Consider simpler query building | +| `ai-chat` | Unknown memory profile | Monitor usage | +| `rollbar` | Queues errors in memory before sending | Ensure queue limits are set | +| `kaminari` + `pagy` | Both pagination gems included | Pick one (pagy is lighter) | +| `carrierwave` + `cloudinary` | File upload handling | Memory spikes during uploads | + +--- + +### 8. Disable Unused Solid* Features + +If students don't use background jobs, Solid Queue adds unnecessary overhead. Similarly for Solid Cable if WebSockets aren't used. + +To disable Solid Queue in Puma, ensure `SOLID_QUEUE_IN_PUMA` is not set. + +To disable entirely, remove from `config/environments/production.rb`: + +```ruby +# Comment out if not using background jobs +# config.active_job.queue_adapter = :solid_queue +``` + +--- + +### 9. Align Connection Pool Size + +The default pool is 5 but `RAILS_MAX_THREADS` would be 2-3. These should match. + +The current `database.yml` already handles this: + +```yaml +pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> +``` + +Change the default from 5 to match your thread count: + +```yaml +pool: <%= ENV.fetch("RAILS_MAX_THREADS", 2) %> +``` + +--- + +## Suggested render.yaml Configuration + +```yaml +services: + - type: web + name: my-app-name + runtime: ruby + plan: free + buildCommand: "./bin/render-build.sh" + startCommand: "./bin/render-start.sh" + envVars: + - key: SECRET_KEY_BASE + generateValue: true + - key: DATABASE_URL + sync: false + - key: WEB_CONCURRENCY + value: 1 # Reduced from 2 + - key: RAILS_MAX_THREADS + value: 2 # Explicitly set lower + - key: MALLOC_ARENA_MAX + value: 2 # Critical for memory fragmentation + - key: RUBY_GC_HEAP_GROWTH_FACTOR + value: 1.1 # Gentler heap growth +``` + +--- + +## Summary Priority Table + +| Priority | Change | Expected Impact | Effort | +|----------|--------|-----------------|--------| +| 1 | `WEB_CONCURRENCY=1` | ~40-50% reduction | Trivial | +| 2 | `MALLOC_ARENA_MAX=2` | ~20-30% reduction | Trivial | +| 3 | Consolidate/simplify databases | ~15-20% reduction | Medium | +| 4 | `RAILS_MAX_THREADS=2` | ~10% reduction | Trivial | +| 5 | Add `preload_app!` to Puma | Better memory sharing | Trivial | +| 6 | Add jemalloc | ~10-20% reduction | Low | +| 7 | Remove duplicate gems (kaminari/pagy) | Minor | Low | + +--- + +## Monitoring + +After implementing changes, monitor memory usage via: + +1. Render.com dashboard metrics +2. Add `get_process_mem` gem for application-level monitoring +3. Consider `derailed_benchmarks` gem for memory profiling during development diff --git a/perf_3.md b/perf_3.md new file mode 100644 index 0000000..130d2ff --- /dev/null +++ b/perf_3.md @@ -0,0 +1,55 @@ +# Render.com Memory Audit (Rails 8 Template) + +This template is intended to run on Render’s free plan, which is memory constrained. Most production OOMs are caused by accidentally running **too many Ruby processes** (Puma workers) and/or running **background jobs inside the web process**. + +## Likely OOM causes in this template (before fixes) + +- **Multiple Puma workers (`WEB_CONCURRENCY > 1`)**: each worker is a full Ruby/Rails process with its own heap. Doubling workers often doubles memory. +- **Solid Queue supervisor running inside Puma** (`SOLID_QUEUE_IN_PUMA=true`): adds job processing threads/processes to the same memory budget as the web server. +- **Not explicitly setting production mode on Render**: if `RAILS_ENV`/`RACK_ENV` aren’t set, you risk “development-ish” behavior (code reloading, extra middleware, dev/test gems), which is significantly heavier. +- **Default-group gems loaded in production**: gems not scoped to `:development`/`:test` get `require`’d in production via `Bundler.require(*Rails.groups)`. + +## Recommended baseline for Render free plan + +Use these defaults unless you’ve upgraded the plan: + +- `RAILS_ENV=production` and `RACK_ENV=production` +- `WEB_CONCURRENCY=1` +- `RAILS_MAX_THREADS=3` (if you still see OOMs, try `2`) +- `MALLOC_ARENA_MAX=2` (helps reduce allocator memory bloat on glibc) +- `BUNDLE_WITHOUT=development:test` +- **Do not run Solid Queue inside Puma**; default jobs to in-process `:async` + +These settings prioritize “stays up” over max throughput, which is usually what you want for student projects. + +## Background jobs: what to do when you need them + +- **Free plan (recommended default)**: keep `RAILS_QUEUE_ADAPTER=async`. + - Pros: lowest memory overhead. + - Cons: jobs are in-memory; jobs can be lost on restart; not suitable for critical background work. + +- **Paid plan / more headroom**: switch back to Solid Queue and run jobs in a separate Render service. + - Web service: `RAILS_QUEUE_ADAPTER=solid_queue`, `SOLID_QUEUE_IN_PUMA=false` + - Worker service (example start command): `./bin/jobs start` + - Keep worker concurrency small at first (e.g. `JOB_CONCURRENCY=1`, and consider reducing `threads` in `config/queue.yml` if needed). + +## Optional further reductions (only if students don’t need these features) + +These are “last mile” wins compared to fixing worker/job concurrency: + +- **Disable unused Rails frameworks** in `config/application.rb` (e.g. Action Cable, Action Text, Action Mailbox, Active Storage) to reduce boot time and memory. +- **Trim default-group gems**: every gem in the default group is loaded in production. If the course doesn’t require a gem for deployed apps, move it to `:development, :test` or remove it. + +## Quick verification checklist (on Render) + +- Confirm the app logs show it booting in **production** (`RAILS_ENV=production`). +- Confirm you only have **one web process** (no extra Puma workers). +- If you’re on the free plan, confirm you are **not** running Solid Queue inside Puma and that jobs are `:async`. + +## Changes applied in this repo + +- `render.yaml` sets production mode explicitly, keeps `WEB_CONCURRENCY=1`, caps thread counts, disables in-Puma Solid Queue, and defaults jobs to `:async`. +- `config/puma.rb` treats `SOLID_QUEUE_IN_PUMA` as a real boolean so `"false"` won’t accidentally enable it. +- `Gemfile` moves dev/test-only gems out of production, and `config/initializers/appdev_support.rb` only initializes in dev/test. +- `bin/render-start.sh` uses `exec` and explicitly binds/ports for Render and defaults to production. + diff --git a/perf_4.md b/perf_4.md new file mode 100644 index 0000000..58a9851 --- /dev/null +++ b/perf_4.md @@ -0,0 +1,108 @@ +# Comprehensive Memory Optimization Plan for Render.com + +This document consolidates the most effective strategies for running a Rails 8 application on Render's free tier (512MB RAM). It balances stability, feature completeness (durable jobs), and resource efficiency. + +## 1. Optimize Web Server Mode (Critical) + +**Strategy:** Force Puma into "Single Mode" (Threads only). +**Why:** Running multiple workers ("Cluster Mode") forks the entire application, doubling memory usage. Single mode is sufficient for most student/low-traffic apps and saves ~40-50% memory. + +**Action:** +- Set `WEB_CONCURRENCY` to `1` in `render.yaml`. +- **Fix `config/puma.rb`:** Ensure it doesn't default to 1 worker (which triggers cluster mode). It should logically result in 0 workers when `WEB_CONCURRENCY` is 1. + +```ruby +# config/puma.rb +web_concurrency = ENV.fetch("WEB_CONCURRENCY", 0).to_i +# Only enable workers if explicitly set > 1 +workers web_concurrency if web_concurrency > 1 +``` + +## 2. Consolidate Database Connections (High Impact) + +**Strategy:** Share a single PostgreSQL database connection pool for the app, cache, queue, and cable. +**Why:** The default `database.yml` defines 4 separate databases (`primary`, `cache`, `queue`, `cable`). Even with a small pool size, maintaining 4 separate pools multiplies the memory overhead of maintaining those connections. +**Impact:** ~15-20% memory reduction. + +**Action:** Update `config/database.yml` production section to share the primary configuration: + +```yaml +production: + primary: &primary_production + <<: *default + url: <%= ENV["DATABASE_URL"] %> + cache: + <<: *primary_production + migrations_paths: db/cache_migrate + queue: + <<: *primary_production + migrations_paths: db/queue_migrate + cable: + <<: *primary_production + migrations_paths: db/cable_migrate +``` + +## 3. Tune Memory Allocator (High Impact) + +**Strategy:** Use Linux memory allocator tuning. +**Why:** Ruby creates high memory fragmentation on the default glibc allocator. +**Impact:** ~20-30% reduction in "bloated" (reserved but unused) memory. + +**Action:** Add to `render.yaml` environment variables: +```yaml +- key: MALLOC_ARENA_MAX + value: 2 +``` + +## 4. Efficient Background Jobs + +**Strategy:** Run Solid Queue inside Puma *but* strictly limit its resource usage. +**Why:** +- **External Worker:** Costs money/RAM you don't have. +- **Async (In-Memory):** Loses jobs on restart (bad for learning durability). +- **Solid Queue in Puma:** Best compromise. It shares the app's loaded memory. + +**Action:** +- Enable `SOLID_QUEUE_IN_PUMA: true`. +- **Crucial:** Ensure database pooling (step 2) is done, otherwise Solid Queue opens its own pool, negating the benefits. + +## 5. Thread Management + +**Strategy:** Cap threads to match the database pool. +**Why:** High thread counts increase memory per request and require larger DB pools. + +**Action:** +- Set `RAILS_MAX_THREADS` to `3` (or `2` if strictly needed) in `render.yaml`. +- Ensure `database.yml` uses this variable for its pool size: `pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>`. + +## 6. Gem Hygiene + +**Strategy:** Audit and remove duplicate/heavy gems. +**Why:** Every loaded gem consumes persistent memory. + +**Action:** +- **Pagination:** Choose *one* (`pagy` is lighter than `kaminari`). Remove the other. +- **Environment Groups:** Ensure gems like `faker`, `rubocop`, `web-console`, `better_errors` are strictly in `group :development, :test`. + +## Summary Configuration (`render.yaml`) + +```yaml +services: + - type: web + # ... + envVars: + - key: RAILS_ENV + value: production + - key: WEB_CONCURRENCY + value: 1 + - key: RAILS_MAX_THREADS + value: 3 + - key: MALLOC_ARENA_MAX + value: 2 + - key: SOLID_QUEUE_IN_PUMA + value: true + - key: SECRET_KEY_BASE + generateValue: true + - key: DATABASE_URL + sync: false +``` diff --git a/render.yaml b/render.yaml index 1f4fda4..53f8e61 100644 --- a/render.yaml +++ b/render.yaml @@ -6,13 +6,26 @@ services: buildCommand: "./bin/render-build.sh" # we already created these two files for you startCommand: "./bin/render-start.sh" envVars: # this section sets some ENV variables needed by Render for deployment + # Render does not guarantee Rails defaults to production mode, so set it explicitly. + - key: RAILS_ENV + value: production + - key: RACK_ENV + value: production - key: SECRET_KEY_BASE generateValue: true - key: DATABASE_URL sync: false + # Keep thread + DB pool counts small to reduce memory usage. + - key: RAILS_MAX_THREADS + value: 3 - key: WEB_CONCURRENCY value: 1 - key: MALLOC_ARENA_MAX value: 2 + # Don't install development/test gems on Render. + - key: BUNDLE_WITHOUT + value: "development:test" + # On Render free plans, running Solid Queue *inside* Puma can push the service over memory limits. + # However, we optimize memory elsewhere (WEB_CONCURRENCY=1, DB consolidation) to allow running it safely here. - key: SOLID_QUEUE_IN_PUMA value: true From 99d316e0f1eefab30ab78269b3a2645ec65f0093 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 29 Dec 2025 18:58:59 -0600 Subject: [PATCH 3/5] Add advanced performance recommendations (perf_5.md) --- perf_5.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 perf_5.md diff --git a/perf_5.md b/perf_5.md new file mode 100644 index 0000000..43e325c --- /dev/null +++ b/perf_5.md @@ -0,0 +1,61 @@ +# Advanced Performance Optimizations + +Based on further analysis of the "Complete Guide to Rails Performance" and your project structure, here are advanced strategies. + +## 1. Disable Unused Rails Frameworks + +**Why:** Rails 8 loads powerful frameworks by default that many simple CRUD apps don't use. Each loaded framework consumes boot-time memory. +**Impact:** Saves ~10-30MB RAM. + +**Action:** +Edit `config/application.rb` and comment out frameworks you aren't using. Common candidates for removal: + +```ruby +require "rails" +# ... +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +# require "action_mailbox/engine" # <-- Disable if not processing inbound email +# require "action_text/engine" # <-- Disable if not using Trix/Rich Text +require "action_view/railtie" +# require "action_cable/engine" # <-- Disable if not using WebSockets +``` + +## 2. Consider Docker Runtime for `jemalloc` + +**Observation:** Your repository contains a production-ready `Dockerfile` that already installs `libjemalloc2`. +**Why:** `jemalloc` is a specialized memory allocator that often performs better than the standard system allocator (`glibc`), reducing fragmentation and overall memory usage by 10-20%. + +**Action:** +Instead of using the "Native Ruby" runtime in Render (`runtime: ruby`), you can switch your Render service to use **Docker**. +1. Update `render.yaml` (or change setting in dashboard) to use `runtime: docker`. +2. This automatically uses your `Dockerfile`, which: + - Installs `jemalloc`. + - Uses `thruster` (an accelerated HTTP/2 proxy) in front of Puma. + - Ensures a consistent, optimized environment. + +*Note: Docker builds on the free tier might be slightly slower than native builds, but the runtime performance is usually superior.* + +## 3. Audit "require 'rails/all'" + +**Check:** Ensure you are NOT using `require "rails/all"` in `config/application.rb`. +**Status:** Your app already correctly requires individual frameworks. (Good job!) + +## 4. Leverage HTTP Caching + +**Why:** Avoiding server work entirely is the best memory optimization. +**Action:** Use `ETags` and `Last-Modified` headers in your controllers for public data. +```ruby +def show + @product = Product.find(params[:id]) + # If the client has a fresh copy, this returns 304 Not Modified immediately + # and stops further processing/rendering. + if stale?(@product) + render :show + end +end +``` From 9050a2cd7e09a6ad677cee127662621c565d21be Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 29 Dec 2025 19:01:18 -0600 Subject: [PATCH 4/5] Add final synthesized performance guide (perf_6.md) --- perf_6.md | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 perf_6.md diff --git a/perf_6.md b/perf_6.md new file mode 100644 index 0000000..ae6033b --- /dev/null +++ b/perf_6.md @@ -0,0 +1,133 @@ +# Ultimate Rails 8 Performance & Memory Guide for Render (Free Tier) + +This guide consolidates the most effective strategies to run a Rails 8 application efficiently on Render.com's Free Tier (512MB RAM). It prioritizes **stability** (avoiding Out-Of-Memory crashes) over maximum throughput. + +--- + +## 🛑 Phase 1: Critical Configuration (The "Must Haves") + +These changes provide the massive memory savings required to fit in 512MB. + +### 1. Force Puma to "Single Mode" +**Why:** Running multiple workers doubles/triples memory usage. 512MB is only enough for **one** Ruby process. +**Action:** +- Set `WEB_CONCURRENCY: 1` in `render.yaml`. +- Ensure `config/puma.rb` respects this: + ```ruby + # config/puma.rb + web_concurrency = ENV.fetch("WEB_CONCURRENCY", 0).to_i + workers web_concurrency if web_concurrency > 1 + ``` + +### 2. Tune Memory Allocator +**Why:** Ruby interacts poorly with the default Linux memory allocator, causing fragmentation ("bloat"). +**Action:** +- Set `MALLOC_ARENA_MAX: 2` in `render.yaml`. + +### 3. Consolidate Database Connections +**Why:** Default Rails 8 setups use 4 separate DB pools (Primary, Cache, Queue, Cable). Maintaining 4 pools wastes connections and memory. +**Action:** +- Modify `config/database.yml` (production) to share the `primary` configuration: + ```yaml + production: + primary: &primary_production + <<: *default + url: <%= ENV["DATABASE_URL"] %> + cache: + <<: *primary_production + migrations_paths: db/cache_migrate + queue: + <<: *primary_production + migrations_paths: db/queue_migrate + cable: + <<: *primary_production + migrations_paths: db/cable_migrate + ``` + +### 4. Cap Thread Count +**Why:** More threads = more memory per request. +**Action:** +- Set `RAILS_MAX_THREADS: 3` in `render.yaml`. +- Ensure `database.yml` pool size matches: `pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>`. + +--- + +## 🧹 Phase 2: Application Hygiene + +Reduce the static footprint of your application. + +### 1. Gemfile Cleanup +**Why:** Every loaded gem eats RAM. +**Action:** +- Move development-only gems (`faker`, `rubocop`, `web-console`, `annotate`, `appdev_support`) to `group :development, :test`. +- Ensure `render.yaml` sets `BUNDLE_WITHOUT: "development:test"`. + +### 2. Disable Unused Frameworks +**Why:** Rails loads Action Mailbox, Action Text, and Active Storage by default. If you don't use them, they are dead weight. +**Action:** +- Comment them out in `config/application.rb`: + ```ruby + # require "action_mailbox/engine" + # require "action_text/engine" + # require "action_cable/engine" # Only if not using WebSockets + ``` + +--- + +## ⚙️ Phase 3: Background Jobs Strategy + +**The Dilemma:** +- **Separate Worker Service:** Costs $$ (Not on free plan). +- **Async Adapter:** Free, low memory, but **loses jobs on restart**. +- **Solid Queue in Puma:** Durable, but uses more shared memory. + +**Recommendation:** +Use **Solid Queue in Puma** but watch memory closely. It is the best balance for a functional student app. + +**Action:** +- Set `SOLID_QUEUE_IN_PUMA: true` in `render.yaml`. +- *Fallback:* If you still hit OOM errors, switch to `RAILS_QUEUE_ADAPTER: async` and `SOLID_QUEUE_IN_PUMA: false`. + +--- + +## 🚀 Phase 4: Advanced Optimizations + +### 1. Use Docker Runtime (for `jemalloc`) +**Why:** Your `Dockerfile` installs `libjemalloc2`, a superior memory allocator that reduces usage by 10-20%. The "Native Ruby" runtime does not use this. +**Action:** +- Switch your Render service Runtime to **Docker**. +- This also gives you `thruster` (static asset acceleration) for free. + +### 2. HTTP Caching +**Why:** The fastest request is one you don't serve. +**Action:** +- Use `stale?` checks in controllers: + ```ruby + def show + @post = Post.find(params[:id]) + render :show if stale?(@post) + end + ``` + +--- + +## ✅ Summary Checklist for `render.yaml` + +```yaml +services: + - type: web + runtime: ruby # or 'docker' for better perf + envVars: + - key: RAILS_ENV + value: production + - key: WEB_CONCURRENCY + value: 1 + - key: RAILS_MAX_THREADS + value: 3 + - key: MALLOC_ARENA_MAX + value: 2 + - key: SOLID_QUEUE_IN_PUMA + value: true + - key: BUNDLE_WITHOUT + value: "development:test" +``` From ec04abd138a3d151a0b474bed5e260677ac561af Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Tue, 26 May 2026 18:11:52 -0500 Subject: [PATCH 5/5] Set page title from @page_title instance variable Switch the layout to read from an @page_title instance variable, falling back to "Default title" when a controller action does not assign one. Controllers can set @page_title directly, which is simpler than threading the value through content_for/yield from each view. --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index bbcc4e6..bc31f4a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,7 @@ <!DOCTYPE html> <html> <head> - <title><%= content_for(:title) || "Rails 8 Template" %> + <%= @page_title || "Default title" %>