• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Performance Comparison: Running Ruby on Rails 7 vs Python (Django) Under Heavy Concurrency Benchmarks

Performance Comparison: Running Ruby on Rails 7 vs Python (Django) Under Heavy Concurrency Benchmarks

Benchmarking Environment Setup

To conduct a fair performance comparison between Ruby on Rails 7 and Python/Django under heavy concurrency, a consistent and controlled environment is paramount. We’ll utilize a cloud-based virtual machine with specific hardware resources and deploy both applications using production-grade web servers and process managers.

Our chosen environment is an AWS EC2 `m5.xlarge` instance, providing 4 vCPUs and 16 GiB of RAM. This offers a reasonable balance of compute and memory for simulating moderate to heavy load. The operating system is Ubuntu 22.04 LTS.

For serving the Rails application, we’ll use Puma, the default web server in Rails 7, configured with a multi-threaded approach. For Django, we’ll employ Gunicorn, a popular Python WSGI HTTP Server, also configured for multi-worker concurrency.

Database for both benchmarks will be PostgreSQL 14, running on a separate RDS instance to avoid I/O contention on the application server. This isolates application-level performance from database bottlenecks.

Application Under Test: Simple API Endpoint

To focus purely on request handling and processing overhead, we’ll create a minimal API endpoint for each framework. This endpoint will perform a simple database query without complex business logic or heavy computation. The goal is to simulate a common read-heavy API scenario.

Rails 7 Application (Minimal API)

We’ll generate a new Rails 7 application and add a simple `Post` model with a `title` and `body` attribute. The API endpoint will fetch a single post by its ID.

1. Project Generation and Model

rails new rails_benchmark --api
cd rails_benchmark
rails generate model Post title:string body:text
rails db:create db:migrate

2. API Controller

# app/controllers/api/v1/posts_controller.rb
module Api
  module V1
    class PostsController << ApplicationController
      def show
        @post = Post.find(params[:id])
        render json: @post, status: :ok
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Post not found" }, status: :not_found
      end
    end
  end
end

3. API Routes

# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts, only: [:show]
    end
  end
end

4. Seeding Data

# db/seeds.rb
100.times do |i|
  Post.create!(title: "Post #{i}", body: "This is the body of post #{i}.")
end
rails db:seed

Python/Django Application (Minimal API)

Similarly, we’ll create a Django project with a simple `Post` model and an API view.

1. Project and App Generation

django-admin startproject django_benchmark
cd django_benchmark
python manage.py startapp api

2. Model Definition

# api/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()

    def __str__(self):
        return self.title

3. Admin Registration (for easy data management)

# api/admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)

4. Migrations and Seeding

python manage.py makemigrations api
python manage.py migrate

For seeding, we’ll use a custom management command.

# api/management/commands/seed_posts.py
from django.core.management.base import BaseCommand
from api.models import Post

class Command(BaseCommand):
    help = 'Seeds the database with sample posts'

    def handle(self, *args, **options):
        if Post.objects.exists():
            self.stdout.write(self.style.WARNING('Posts already exist. Skipping seeding.'))
            return

        num_posts = 100
        for i in range(num_posts):
            Post.objects.create(title=f'Post {i}', body=f'This is the body of post {i}.')
        self.stdout.write(self.stdout.style.SUCCESS(f'Successfully seeded {num_posts} posts.'))
python manage.py seed_posts

5. API View

# api/views.py
from django.http import JsonResponse, Http404
from .models import Post

def get_post(request, post_id):
    try:
        post = Post.objects.get(pk=post_id)
        return JsonResponse({
            'id': post.id,
            'title': post.title,
            'body': post.body
        })
    except Post.DoesNotExist:
        raise Http404("Post does not exist")

6. URL Configuration

# api/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('posts//', views.get_post, name='get_post'),
]
# django_benchmark/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
]

Production Deployment Configuration

Proper configuration of the web server and process manager is critical for achieving high concurrency. We’ll tune parameters for both Puma (Rails) and Gunicorn (Django).

Rails 7 with Puma

Puma’s concurrency model is based on threads and workers. For a CPU-bound application, a balance between workers and threads is key. We’ll aim for a configuration that leverages the 4 vCPUs of our instance.

1. Puma Configuration (`config/puma.rb`)

# config/puma.rb
workers Integer(ENV.fetch("WEB_CONCURRENCY") { 2 }) # Number of worker processes
threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS") { 5 }) # Max threads per worker

threads threads_count, threads_count # Min and Max threads per worker

preload_app!

environment ENV.fetch("RAILS_ENV") { "production" }

plugin :tmp_restart

# Allow Puma to be restarted by `rails restart` command.
plugin :restart

on_worker_boot do
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
end

In this setup, we’re using 2 workers and 5 threads per worker. This gives us a total of 10 concurrent request handlers (2 workers * 5 threads). The `preload_app!` directive helps to load the application code once per worker, reducing startup time for subsequent requests.

2. Environment Variables and Startup Command

export RAILS_ENV=production
export WEB_CONCURRENCY=2
export RAILS_MAX_THREADS=5
bundle exec puma -C config/puma.rb

Python/Django with Gunicorn

Gunicorn uses a worker process model. We’ll configure it to use multiple worker processes, each potentially with its own threads (though for simplicity and common practice, we’ll focus on worker processes first).

1. Gunicorn Configuration (Command Line)

gunicorn --workers 4 --threads 2 --bind 0.0.0.0:8000 django_benchmark.wsgi:application

Here, we’re using 4 worker processes. The `–threads 2` option enables multi-threading within each worker. This results in 8 concurrent request handlers (4 workers * 2 threads). The optimal number of workers is often recommended to be `(2 * number_of_cores) + 1`. For our 4 vCPUs, this would suggest around 9 workers. However, we’ll start with 4 workers and 2 threads to keep it comparable to the Rails configuration’s total handler count.

Benchmarking Tool and Methodology

We’ll use `wrk`, a modern HTTP benchmarking tool, known for its high performance and ease of use. It can generate significant load and measure latency and throughput accurately.

1. Installing `wrk`

# On Ubuntu/Debian
sudo apt update
sudo apt install wrk

# Or compile from source for latest version
# git clone https://github.com/wg/wrk.git
# cd wrk
# make
# sudo cp wrk /usr/local/bin/

2. Benchmark Script

We’ll run benchmarks with varying numbers of concurrent connections and requests per connection to simulate different load scenarios. The target URL will be a specific post ID, e.g., `http://your_server_ip/api/v1/posts/1` for Rails or `http://your_server_ip/api/posts/1` for Django.

Scenario 1: Moderate Concurrency (100 connections, 10 requests each)

# For Rails
wrk -t4 -c100 -d30s -R10 http://your_server_ip/api/v1/posts/1

# For Django
wrk -t4 -c100 -d30s -R10 http://your_server_ip/api/posts/1

Scenario 2: High Concurrency (500 connections, 10 requests each)

# For Rails
wrk -t4 -c500 -d30s -R10 http://your_server_ip/api/v1/posts/1

# For Django
wrk -t4 -c500 -d30s -R10 http://your_server_ip/api/posts/1

Scenario 3: Very High Concurrency (1000 connections, 10 requests each)

# For Rails
wrk -t4 -c1000 -d30s -R10 http://your_server_ip/api/v1/posts/1

# For Django
wrk -t4 -c1000 -d30s -R10 http://your_server_ip/api/posts/1

We use `-t4` to utilize 4 threads for `wrk` itself, ensuring `wrk` isn’t the bottleneck. The duration `-d30s` is set to 30 seconds, and `-R10` means 10 requests per connection. We’ll record the Requests/sec and Latency (Avg, Max, 99th percentile).

Benchmark Results and Analysis

After running the benchmarks, we’ll analyze the output from `wrk`. The key metrics to compare are:

  • Requests/sec (RPS): Higher is better, indicating more requests processed per unit of time.
  • Latency (Avg, 99th percentile): Lower is better. High 99th percentile latency indicates that a small fraction of requests are experiencing significant delays, which is critical for user experience.

(Note: Actual benchmark results will vary based on exact environment, OS tuning, and specific application code. The following is a hypothetical representation of typical outcomes.)

Hypothetical Results Summary

Scenario 1: Moderate Concurrency (100 connections)

Rails 7:
Requests/sec: 12,500
Latency (Avg): 7.8 ms
Latency (99th percentile): 25 ms

Django:
Requests/sec: 14,000
Latency (Avg): 7.0 ms
Latency (99th percentile): 22 ms

Scenario 2: High Concurrency (500 connections)

Rails 7:
Requests/sec: 10,000
Latency (Avg): 48 ms
Latency (99th percentile): 150 ms

Django:
Requests/sec: 11,500
Latency (Avg): 42 ms
Latency (99th percentile): 130 ms

Scenario 3: Very High Concurrency (1000 connections)

Rails 7:
Requests/sec: 7,500
Latency (Avg): 120 ms
Latency (99th percentile): 400 ms

Django:
Requests/sec: 9,000
Latency (Avg): 100 ms
Latency (99th percentile): 350 ms

Analysis of Results

In this specific, simplified benchmark scenario:

  • Django consistently shows slightly higher throughput (Requests/sec) and lower latency across all concurrency levels. This is often attributable to Python’s C-based extensions and potentially more efficient memory management for I/O-bound tasks in its standard libraries and WSGI servers.
  • As concurrency increases, both frameworks experience a degradation in performance, which is expected. However, Django appears to handle the increased load with a slightly gentler performance curve.
  • The 99th percentile latency is a critical indicator. While both frameworks show an increase, Django’s values remain marginally better, suggesting a more consistent experience for the majority of users under stress.

It’s crucial to note that these results are for a *very* basic read operation. Real-world applications involve more complex logic, object-relational mapping (ORM) overhead, serialization, and potentially external API calls. The performance characteristics can shift significantly based on these factors.

Further Optimization and Considerations

The configurations used are a starting point. Significant performance gains can be achieved through further tuning:

Rails 7 Optimizations

  • Database Connection Pooling: Ensure `pool` size in `config/database.yml` is adequate for the number of threads/workers.
  • Caching: Implement fragment caching, page caching, or low-level cache stores (e.g., Redis) for frequently accessed data.
  • Background Jobs: Offload non-critical tasks (emailing, image processing) to background job processors like Sidekiq or Delayed Job.
  • Puma Tuning: Experiment with different worker/thread counts. Consider using `preload_app!` with `fork_workers` for better memory utilization if applicable.
  • JIT Compilation: Ruby 3.x has a JIT compiler. Ensure it’s enabled and consider its impact.

Django Optimizations

  • Database Connection Pooling: Use libraries like `django-db-connection-pool` or configure Gunicorn workers to manage connections efficiently.
  • Caching: Django’s built-in caching framework (Memcached, Redis) is highly effective.
  • Asynchronous Views: For I/O-bound operations, consider using Django’s async views with an ASGI server like Uvicorn.
  • Gunicorn Tuning: Experiment with worker types (sync, gevent, eventlet) and worker/thread counts.
  • Serialization: Optimize Django REST Framework serializers if used.

General Considerations

  • Database Performance: Indexing, query optimization, and database server tuning are critical for both frameworks.
  • Network Latency: Ensure the application server and database are geographically close.
  • Load Balancer: In a production environment, a load balancer (e.g., Nginx, HAProxy, AWS ELB) is essential for distributing traffic and managing application instances.
  • Profiling: Use profiling tools (e.g., `ruby-prof`, `stackprof` for Rails; `cProfile` for Django) to identify bottlenecks within the application code itself.

The choice between Rails and Django often extends beyond raw performance benchmarks. Factors like developer productivity, ecosystem maturity, team expertise, and specific project requirements play a significant role. However, for raw, I/O-bound request handling under heavy concurrency, this benchmark suggests Python/Django may hold a slight edge, though Rails 7 is highly competitive and can be optimized to achieve excellent performance.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala