Performance Comparison: Running Python (FastAPI) vs Laravel 11 Under Heavy Concurrency Benchmarks
Benchmarking Environment Setup
To conduct a fair and reproducible performance comparison between Python’s FastAPI and Laravel 11 under heavy concurrency, a consistent and isolated environment is paramount. We will utilize Docker Compose to orchestrate our services, ensuring identical operating system versions, PHP/Python runtimes, and web server configurations for both frameworks.
The benchmarking tool of choice is wrk, a modern HTTP benchmarking tool capable of generating high concurrency loads. We’ll configure wrk to simulate realistic user traffic by making requests to a simple API endpoint designed to perform a minimal amount of work, thereby isolating the performance of the web frameworks themselves.
Docker Compose Configuration
Our docker-compose.yml will define three primary services: the application itself (either FastAPI or Laravel), a reverse proxy (Nginx), and a database (PostgreSQL, for a realistic scenario, though its impact will be minimized in the benchmark endpoint).
First, the base docker-compose.yml:
version: '3.8'
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: benchmark_db
POSTGRES_USER: benchmark_user
POSTGRES_PASSWORD: benchmark_password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
nginx:
image: nginx:stable-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
networks:
- app-network
app:
build:
context: .
dockerfile: Dockerfile
networks:
- app-network
depends_on:
- db
volumes:
postgres_data:
networks:
app-network:
driver: bridge
FastAPI Application and Dockerfile
For FastAPI, we’ll use Uvicorn as the ASGI server, running behind Nginx. The benchmark endpoint will be a simple “hello world” with a small delay to simulate some processing.
app/main.py:
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/")
async def read_root():
await asyncio.sleep(0.001) # Simulate minimal I/O or processing
return {"message": "Hello from FastAPI"}
app/requirements.txt:
fastapi uvicorn[standard]
Dockerfile (for FastAPI):
FROM python:3.11-slim WORKDIR /app COPY ./app /app RUN pip install --no-cache-dir -r requirements.txt CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
nginx.conf (for FastAPI):
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Laravel 11 Application and Dockerfile
For Laravel 11, we’ll use the built-in Octane with Swoole for high-performance, concurrent request handling. Nginx will again act as the reverse proxy.
Assuming a fresh Laravel 11 project is created and Octane is installed:
app/Http/Controllers/BenchmarkController.php:
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
class BenchmarkController extends Controller
{
public function index(): JsonResponse
{
// Simulate minimal processing
usleep(1000); // 1ms delay
return response()->json(['message' => 'Hello from Laravel']);
}
}
routes/web.php (or routes/api.php):
use App\Http\Controllers\BenchmarkController;
Route::get('/', [BenchmarkController::class, 'index']);
Dockerfile (for Laravel with Octane/Swoole):
FROM php:8.3-fpm-alpine
RUN apk add --no-cache \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
icu-dev \
oniguruma-dev \
libxml2-dev \
zlib-dev \
postgresql-dev \
nodejs \
npm
RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd zip pdo pdo_pgsql
RUN pecl install swoole \
&& docker-php-ext-enable swoole
WORKDIR /var/www/html
COPY --chown=www-data:www-data . .
RUN composer install --no-dev --optimize-autoloader
RUN php artisan octane:install --swoole
RUN php artisan config:cache
RUN php artisan route:cache
CMD ["php", "artisan", "octane", "start", "--host=0.0.0.0", "--port=8000", "--workers=auto", "--max-requests=5000"]
nginx.conf (for Laravel):
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000; # Assuming PHP-FPM is running on port 9000
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
# For Octane with Swoole, Nginx acts as a proxy to the Octane server
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Note: For Laravel, the Dockerfile assumes you’ve already run composer create-project laravel/laravel laravel-app and then installed Octane and Swoole. The CMD in the Dockerfile is crucial for starting Octane with Swoole. The Nginx configuration also needs to be adjusted to proxy to the Octane server (port 8000) rather than PHP-FPM.
Benchmarking Procedure
Once the Docker Compose setup is ready for either framework, we can proceed with the benchmarking. Ensure you have wrk installed on your local machine or within a separate Docker container that can reach the Nginx service.
The command to run the benchmark will look like this:
wrk -t16 -c1000 -d30s --latency http://localhost/
Let’s break down the wrk command:
-t16: Use 16 threads. Adjust based on your CPU cores.-c1000: Maintain 1000 concurrent connections. This is the primary concurrency metric.-d30s: Run the benchmark for 30 seconds.--latency: Record latency statistics.http://localhost/: The target URL.
Execution and Analysis
To run the benchmark for FastAPI:
# Navigate to the directory containing docker-compose.yml and Dockerfile for FastAPI docker compose up -d # Wait for services to start sleep 10 # Run wrk wrk -t16 -c1000 -d30s --latency http://localhost/ # Stop services docker compose down
To run the benchmark for Laravel 11 (with Octane/Swoole):
# Navigate to the directory containing docker-compose.yml and Dockerfile for Laravel docker compose up -d # Wait for services to start sleep 10 # Run wrk wrk -t16 -c1000 -d30s --latency http://localhost/ # Stop services docker compose down
The output from wrk will provide key metrics such as:
Requests/sec: The throughput of the application. Higher is better.Latency: Average, median, and percentile latencies (e.g., 99th percentile). Lower is better.Transfer/sec: Bandwidth usage.
Interpreting Results:
When comparing the results, pay close attention to the Requests/sec and the 99th percentile latency. A framework that can sustain a higher request rate with lower tail latencies is generally considered more performant under load.
FastAPI, being a minimalist framework built on Starlette and Pydantic, often exhibits lower overhead. Its asynchronous nature, when properly utilized with an ASGI server like Uvicorn, allows it to handle I/O-bound tasks very efficiently. The Python GIL (Global Interpreter Lock) can become a bottleneck for CPU-bound tasks, but for typical web API workloads that are I/O-bound, it performs exceptionally well.
Laravel 11 with Octane and Swoole aims to bridge the gap by providing a persistent, multi-threaded server environment. Swoole is a high-performance C extension for PHP that enables asynchronous I/O and coroutines. This allows Laravel applications to bypass the traditional request-response cycle overhead of PHP-FPM, leading to significant performance gains, especially under high concurrency. The results here will indicate how effectively Laravel’s ecosystem and Octane’s optimizations can compete with a native ASGI framework.
Advanced Considerations and Tuning
Several factors can influence the benchmark results and should be considered for production tuning:
- Worker Processes/Threads:
- For Uvicorn (FastAPI), the number of workers can be configured (e.g.,
uvicorn main:app --workers 4). - For Swoole (Laravel Octane),
--workers=autoattempts to determine an optimal number, but manual tuning (e.g.,--workers=8) might be necessary. The optimal number is often related to the number of CPU cores. - Database Connections: In a real-world scenario, database connection pooling is critical. Both frameworks have different strategies for managing database connections. For these benchmarks, we’ve minimized database interaction, but for more complex endpoints, this would be a major factor.
- Caching: Implementing caching strategies (e.g., Redis, Memcached) at various levels can drastically improve performance.
- Nginx Configuration: Tuning Nginx worker processes, connection limits, and keepalive settings can also impact overall throughput.
- PHP/Python Version: Newer versions often bring performance improvements. Ensure you are using recent, stable releases.
- Swoole/Uvicorn Configuration: Explore specific tuning parameters for Uvicorn (e.g.,
--loop uvloop) and Swoole (e.g.,--max-requests,--task-workers) that are not covered by the basic Octane setup.
By systematically setting up the environment, running controlled benchmarks, and analyzing the results with these advanced considerations in mind, you can make an informed decision about which framework best suits your application’s performance requirements under heavy concurrency.