LoadForge LogoLoadForge

Ruby on Rails Load Testing Guide with LoadForge

Ruby on Rails Load Testing Guide with LoadForge

Introduction

Ruby on Rails makes it fast to build feature-rich web applications, but performance under real traffic depends on far more than elegant controllers and Active Record models. As your Rails app grows, bottlenecks can appear in database queries, session handling, background job coordination, asset delivery, caching layers, and external API integrations. That’s why load testing Ruby on Rails applications is essential before traffic spikes, product launches, or major infrastructure changes.

A proper load testing strategy helps you understand how your Rails application behaves under expected load, peak load, and stress conditions. With LoadForge, you can run cloud-based distributed load testing using Locust scripts written in Python, simulate realistic user behavior, and analyze real-time reporting across global test locations. Whether you’re testing a monolithic Rails app, a Rails API backend, or a multi-tenant SaaS platform, LoadForge gives you the tools to identify slow endpoints, saturation points, and scaling limits.

In this Ruby on Rails load testing guide, you’ll learn how to create realistic Locust scripts for Rails, test authenticated flows, exercise database-heavy endpoints, and interpret the results to improve application performance.

Prerequisites

Before you start load testing a Ruby on Rails application with LoadForge, make sure you have:

  • A running Rails application in a staging or performance environment
  • Test user accounts and seeded data
  • Knowledge of your key user journeys, such as login, product browsing, checkout, dashboard access, or API usage
  • Authentication details, such as session-based login, CSRF requirements, or token-based auth for Rails APIs
  • A LoadForge account for running distributed load tests at scale
  • Visibility into app metrics if possible, including:
    • Puma or Unicorn worker utilization
    • PostgreSQL or MySQL performance
    • Redis usage
    • Sidekiq queue depth
    • Nginx or CDN metrics
    • Rails logs and APM traces

You should also confirm that:

  • Caching is configured as it would be in production
  • Background jobs are enabled if they are part of the user workflow
  • Rate limiting, WAF rules, or anti-bot protections are accounted for
  • You are not running tests against production unless specifically planned and approved

If your Rails app uses CSRF protection with standard session login, you’ll need to simulate that correctly in your Locust scripts. If it’s an API-only Rails app using JWT or bearer tokens, your scripts should reflect that authentication flow.

Understanding Ruby on Rails Under Load

Ruby on Rails performance depends on the full request lifecycle, not just controller code. Under load, several common bottlenecks tend to appear.

Application server concurrency

Rails apps are often deployed with Puma, which uses threads and workers. Your effective concurrency depends on:

  • Number of Puma workers
  • Threads per worker
  • Available CPU and memory
  • Ruby VM behavior
  • Blocking operations in application code

If requests spend too much time waiting on the database or external services, Puma threads can become saturated quickly.

Database pressure

For many Rails applications, the database becomes the first major bottleneck. Common issues include:

  • N+1 queries in views or serializers
  • Missing indexes
  • Slow joins or large table scans
  • Expensive aggregate queries
  • Lock contention during writes
  • Connection pool exhaustion

Load testing reveals these issues when response times climb as concurrency increases.

Session and authentication overhead

Traditional Rails apps often use cookie-based sessions with CSRF validation. Under load, login flows can become expensive if they involve:

  • Repeated session creation
  • Password hashing overhead
  • Database lookups for every request
  • Excessive redirects

Rendering and serialization cost

Rails can spend significant time rendering ERB templates, JSON serializers, or partial-heavy pages. API-only apps may suffer from:

  • Large JSON payload generation
  • Inefficient serializers
  • Over-fetching associations
  • Repeated object allocations

Caching behavior

Caching can dramatically improve Rails performance, but only if it’s working correctly. Load testing can expose:

  • Cache stampedes
  • Low hit rates
  • Fragment cache invalidation issues
  • Redis saturation
  • Overdependence on uncached pages

Background jobs and external dependencies

A user request may trigger Sidekiq jobs, emails, search indexing, or third-party API calls. Even if these happen asynchronously, load testing can reveal downstream pressure that affects user experience indirectly.

This is why performance testing Rails requires realistic user flows rather than just hammering a single endpoint.

Writing Your First Load Test

Let’s start with a basic load test for a typical Rails e-commerce app. This example simulates anonymous browsing behavior across common storefront pages.

Basic Rails page browsing test

python
from locust import HttpUser, task, between
 
class RailsStoreUser(HttpUser):
    wait_time = between(1, 3)
 
    @task(4)
    def homepage(self):
        self.client.get("/", name="GET /")
 
    @task(3)
    def products_index(self):
        self.client.get("/products", name="GET /products")
 
    @task(2)
    def product_detail(self):
        product_id = 101
        self.client.get(f"/products/{product_id}", name="GET /products/:id")
 
    @task(1)
    def category_page(self):
        self.client.get("/categories/running-shoes", name="GET /categories/:slug")

What this script does

This Locust script models a simple anonymous user browsing a Rails storefront. It targets realistic Rails routes:

  • / for the homepage
  • /products for the product listing
  • /products/:id for a product detail page
  • /categories/:slug for category filtering

The @task weights reflect typical traffic patterns where homepage and listing pages receive more traffic than category or product detail pages.

Why this matters for Rails

Even this simple test can uncover important Rails performance issues:

  • Slow controller actions
  • Heavy view rendering
  • Fragment caching problems
  • Database load from product listing queries
  • Missing indexes on category filters
  • Asset or CDN misconfiguration affecting page delivery

When you run this test in LoadForge, you can scale it across multiple cloud-based generators and use real-time reporting to see response times, failure rates, and throughput as concurrency increases.

Advanced Load Testing Scenarios

Basic page hits are a good start, but real Ruby on Rails load testing should include authenticated flows, form submissions, and API endpoints. Below are more realistic scenarios that Rails developers commonly need to test.

Scenario 1: Session-based login with CSRF protection

Many Rails apps use Devise or a similar authentication system with CSRF validation. This test simulates a realistic login flow.

python
from locust import HttpUser, task, between
from bs4 import BeautifulSoup
 
class RailsAuthenticatedUser(HttpUser):
    wait_time = between(2, 5)
 
    def on_start(self):
        self.login()
 
    def login(self):
        response = self.client.get("/users/sign_in", name="GET /users/sign_in")
        soup = BeautifulSoup(response.text, "html.parser")
        csrf_token = soup.find("meta", attrs={"name": "csrf-token"})["content"]
 
        login_data = {
            "user[email]": "loadtest.user@example.com",
            "user[password]": "Password123!"
        }
 
        headers = {
            "X-CSRF-Token": csrf_token,
            "Referer": f"{self.host}/users/sign_in"
        }
 
        self.client.post(
            "/users/sign_in",
            data=login_data,
            headers=headers,
            name="POST /users/sign_in"
        )
 
    @task(3)
    def dashboard(self):
        self.client.get("/dashboard", name="GET /dashboard")
 
    @task(2)
    def account_orders(self):
        self.client.get("/account/orders", name="GET /account/orders")
 
    @task(1)
    def account_profile(self):
        self.client.get("/account/profile", name="GET /account/profile")

Why this is realistic for Rails

Rails applications often require:

  • A GET request to retrieve the login form
  • Extraction of the CSRF token
  • A POST to submit credentials
  • Session cookies managed automatically by the client

This script closely mirrors how a browser interacts with a Rails app using Devise. It is much more accurate than simply posting credentials directly without first loading the form.

What to watch for

When load testing authenticated Rails flows, monitor for:

  • Slow password verification
  • Session store bottlenecks
  • Database lookups on every authenticated request
  • Redirect loops or 401/422 errors
  • Increased latency on dashboard pages with many associations

If your session store uses Redis or the database, this test can also reveal pressure on that layer.

Scenario 2: Rails JSON API with token authentication

Many teams use Rails as an API backend for SPAs or mobile applications. This example tests a Rails API using a token-based login flow and exercises realistic endpoints.

python
from locust import HttpUser, task, between
import random
 
class RailsApiUser(HttpUser):
    wait_time = between(1, 2)
 
    def on_start(self):
        self.authenticate()
 
    def authenticate(self):
        response = self.client.post(
            "/api/v1/auth/sign_in",
            json={
                "email": "api.tester@example.com",
                "password": "Password123!"
            },
            name="POST /api/v1/auth/sign_in"
        )
 
        token = response.json().get("token")
        self.client.headers.update({
            "Authorization": f"Bearer {token}",
            "Accept": "application/json"
        })
 
    @task(4)
    def list_projects(self):
        self.client.get("/api/v1/projects", name="GET /api/v1/projects")
 
    @task(3)
    def project_detail(self):
        project_id = random.choice([12, 18, 24, 31])
        self.client.get(f"/api/v1/projects/{project_id}", name="GET /api/v1/projects/:id")
 
    @task(2)
    def list_tasks(self):
        project_id = random.choice([12, 18, 24, 31])
        self.client.get(
            f"/api/v1/projects/{project_id}/tasks?status=open&page=1",
            name="GET /api/v1/projects/:id/tasks"
        )
 
    @task(1)
    def create_comment(self):
        task_id = random.choice([201, 202, 203, 204])
        self.client.post(
            f"/api/v1/tasks/{task_id}/comments",
            json={
                "comment": {
                    "body": "Load test comment from LoadForge performance suite"
                }
            },
            name="POST /api/v1/tasks/:id/comments"
        )

Why this matters

This script models a Rails API backend with realistic nested routes and JSON payloads. It tests both read-heavy and write-heavy operations:

  • Listing projects
  • Fetching project details
  • Querying filtered tasks
  • Creating comments

This is useful for performance testing Rails APIs built with Jbuilder, ActiveModel Serializers, Blueprinter, or custom JSON rendering.

Rails-specific bottlenecks this can reveal

  • Slow serializers
  • Large payload generation
  • N+1 queries in nested resources
  • Authorization overhead from Pundit or CanCanCan
  • Database contention on comment creation
  • Connection pool exhaustion under mixed read/write traffic

Scenario 3: Add-to-cart and checkout flow

For Rails commerce applications, one of the most important load testing scenarios is a multi-step transactional flow. This example simulates browsing, adding to cart, and beginning checkout.

python
from locust import HttpUser, task, between, SequentialTaskSet
from bs4 import BeautifulSoup
import random
 
class CheckoutFlow(SequentialTaskSet):
    def on_start(self):
        self.product_id = random.choice([101, 102, 103, 104])
 
    @task
    def view_product(self):
        self.client.get(f"/products/{self.product_id}", name="GET /products/:id")
 
    @task
    def add_to_cart(self):
        product_page = self.client.get(f"/products/{self.product_id}")
        soup = BeautifulSoup(product_page.text, "html.parser")
        csrf_token = soup.find("meta", attrs={"name": "csrf-token"})["content"]
 
        headers = {
            "X-CSRF-Token": csrf_token,
            "Referer": f"{self.user.host}/products/{self.product_id}"
        }
 
        self.client.post(
            "/cart_items",
            data={
                "product_id": self.product_id,
                "quantity": 1
            },
            headers=headers,
            name="POST /cart_items"
        )
 
    @task
    def view_cart(self):
        self.client.get("/cart", name="GET /cart")
 
    @task
    def begin_checkout(self):
        cart_page = self.client.get("/checkout")
        soup = BeautifulSoup(cart_page.text, "html.parser")
        csrf_token = soup.find("meta", attrs={"name": "csrf-token"})["content"]
 
        headers = {
            "X-CSRF-Token": csrf_token,
            "Referer": f"{self.user.host}/checkout"
        }
 
        self.client.post(
            "/orders",
            data={
                "order[shipping_address]": "123 Test Street",
                "order[city]": "Austin",
                "order[state]": "TX",
                "order[postal_code]": "78701",
                "order[country]": "US",
                "order[payment_method]": "credit_card"
            },
            headers=headers,
            name="POST /orders"
        )
 
class RailsCheckoutUser(HttpUser):
    wait_time = between(2, 4)
    tasks = [CheckoutFlow]

Why this is valuable

This scenario tests a critical Rails business workflow with multiple steps and stateful interactions. It is especially useful for identifying:

  • Cart session issues
  • CSRF handling problems
  • Slow order creation transactions
  • Inventory locking or validation delays
  • Database write amplification
  • Payment orchestration overhead before gateway handoff

This kind of end-to-end stress testing is often far more revealing than isolated endpoint tests because it mirrors real customer behavior.

Analyzing Your Results

After running your Ruby on Rails load test in LoadForge, the next step is understanding what the results actually mean.

Key metrics to review

Focus on these core performance testing metrics:

  • Response time percentiles, especially p95 and p99
  • Requests per second
  • Error rate
  • Concurrent users
  • Time to first failure
  • Throughput changes as load increases

Average response time alone is not enough. Rails apps often look fine on average while p95 or p99 latency becomes unacceptable under concurrency.

Map slow endpoints to Rails internals

When you see slow routes, tie them back to likely Rails components:

  • Slow /products page: expensive Active Record queries, missing indexes, fragment cache misses
  • Slow /dashboard: too many associations loaded, N+1 queries, heavy partial rendering
  • Slow /api/v1/projects/:id/tasks: inefficient serializers, authorization checks, pagination issues
  • Slow POST /orders: transaction contention, callbacks, validations, external integrations

Correlate with infrastructure metrics

LoadForge’s real-time reporting helps you spot when latency rises, but you should also compare results with application metrics:

  • CPU saturation on app servers
  • Database CPU or IOPS spikes
  • Connection pool exhaustion
  • Redis latency
  • Sidekiq backlog growth
  • Memory pressure and GC activity

For Rails specifically, APM tools can help confirm whether the bottleneck is in SQL, rendering, external calls, or Ruby code execution.

Use distributed testing for realism

If your users are geographically distributed, LoadForge’s global test locations can simulate traffic from different regions. This is useful for:

  • Measuring CDN effectiveness
  • Testing latency-sensitive Rails pages
  • Validating regional infrastructure
  • Understanding user experience outside your primary deployment region

Test incrementally

A good Rails load testing plan usually includes:

  • Baseline load testing at normal traffic
  • Peak load testing at expected busy periods
  • Stress testing beyond expected load
  • Soak testing for memory leaks or connection issues over time

Because LoadForge supports cloud-based infrastructure and CI/CD integration, you can automate these tests as part of release validation.

Performance Optimization Tips

Once your load test reveals bottlenecks, these are some of the most effective ways to improve Ruby on Rails performance.

Fix N+1 queries

Use eager loading with includes, preload, or eager_load where appropriate. N+1 issues are one of the most common causes of Rails slowdowns under concurrent traffic.

Add and validate indexes

If load testing shows slow filtering, sorting, or joins, review your database indexes. Rails apps often degrade quickly when high-traffic queries lack proper indexing.

Tune Puma and database pool settings

Make sure your Puma thread count aligns with your database connection pool. A mismatch can cause request queuing or connection starvation.

Cache strategically

Use fragment caching, low-level caching, and HTTP caching where appropriate. For API responses, consider caching expensive serialized payloads when business rules allow.

Reduce view and serializer overhead

Optimize partial rendering, avoid unnecessary object allocations, and trim oversized JSON responses. Rails apps with complex serializers often benefit from payload reduction.

Move expensive work to background jobs

If requests trigger emails, reports, search indexing, or webhooks, move that work to Sidekiq or another job processor so user-facing response times stay low.

Audit callbacks and validations

Long request times on create or update actions often come from model callbacks, synchronous integrations, or overly complex validations.

Profile authentication flows

If login becomes slow under load, check password hashing cost, session store efficiency, and repeated user lookups.

Common Pitfalls to Avoid

Load testing Ruby on Rails apps is most effective when the test environment and scripts reflect reality. Avoid these common mistakes.

Ignoring CSRF and session behavior

Rails often requires form authenticity tokens and session cookies. If your script skips them, your test may not reflect real user behavior or may generate misleading errors.

Testing only the homepage

A Rails app’s homepage is rarely the most expensive route. Include dashboards, filtered listings, search, checkout, and API writes in your performance testing plan.

Using unrealistic test data

If all users hit the same record or submit identical payloads, you may miss important scaling issues. Use varied IDs, realistic form data, and seeded datasets.

Forgetting background job impact

A request may return quickly while Sidekiq queues pile up behind the scenes. Monitor asynchronous systems during load testing.

Overlooking database state changes

Write-heavy tests can change the environment in ways that affect later runs. Reset or reseed data between tests when necessary.

Running tests without observability

Load testing tells you what is slow, but not always why. Pair LoadForge results with Rails logs, APM, database metrics, and infrastructure monitoring.

Not ramping gradually

Jumping immediately to very high concurrency can create noisy results. Use staged ramp-up patterns to identify where performance starts to degrade.

Conclusion

Ruby on Rails can scale extremely well, but only when you understand how it behaves under realistic traffic. Load testing helps you uncover slow queries, rendering bottlenecks, session issues, API serialization problems, and infrastructure limits before your users experience them in production.

With LoadForge, you can build realistic Locust-based Rails load tests, simulate authenticated user journeys, run distributed testing from global locations, and analyze results in real time. Whether you’re validating a new release, preparing for a traffic spike, or performing ongoing stress testing as part of CI/CD, LoadForge gives you a practical way to improve Rails performance with confidence.

If you’re ready to find bottlenecks, improve response times, and scale your Ruby on Rails application reliably, try LoadForge and start building your first Rails load test today.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.