Skip to content

YanGusik/ta_benchmark

Repository files navigation

ta_benchmark

Benchmark comparing different Laravel server adapters under load using k6.

Adapters

Adapter Port Workers Image
PHP-FPM + nginx 8082 12 php:8.4-fpm-alpine + nginx:alpine
Octane Swoole 8084 12 phpswoole/swoole:5.1-php8.3-alpine
TrueAsync FrankenPHP 8083 12 trueasync/php-true-async:latest-frankenphp

Routes tested

Route Description
GET /hello Pure JSON response, no DB
GET /test One DB query (SELECT pg_sleep(0.01), simulates 10ms DB latency)
GET /bench 5 DB queries: SELECT user, SELECT posts, INSERT view, UPDATE counter, SELECT aggregate
GET /debug/connections Live PostgreSQL connection stats (active, idle, total vs max) — TrueAsync, Swoole only

Setup

Requirements

  • Docker + Docker Compose
  • k6

Clone

git clone https://github.com/YanGusik/ta_benchmark.git
cd ta_benchmark

PHP-FPM

cd fpm

# 1. Build the image, then install dependencies before starting
#    (the container crashes on startup if vendor/ is missing)
docker compose build
docker compose run --rm php composer install --ignore-platform-reqs

# 2. Start services
docker compose up -d

# 3. First-time setup
docker compose exec php chmod -R 777 /app/storage /app/bootstrap/cache
docker compose exec php php artisan key:generate
docker compose exec php php artisan migrate
docker compose exec php php artisan db:seed --class=BenchmarkSeeder
curl http://localhost:8082/hello

Octane Swoole

cd octane_swoole

# 1. Build the image, then install dependencies before starting
docker compose build
docker compose run --rm app composer install --ignore-platform-reqs

# 2. Start services
docker compose up -d

# 3. First-time setup
docker compose exec app chmod -R 777 /app/storage /app/bootstrap/cache
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed --class=BenchmarkSeeder
curl http://localhost:8084/hello

TrueAsync FrankenPHP

TrueAsync uses a custom PHP extension for coroutines and requires PHP 8.6 (only available inside the Docker image — not a standard PHP release). The package yangusik/laravel-spawn is pulled from a VCS repository. Use --ignore-platform-reqs so Composer doesn't reject the PHP 8.6 requirement on your local machine.

cd trueasync

# 1. Install dependencies before starting
#    (uses pre-built image — no build step needed)
docker compose run --rm app composer install --ignore-platform-reqs

# 2. Start the server
docker compose up -d

# 3. First-time setup
docker compose exec app chmod -R 777 /app/storage /app/bootstrap/cache
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed --class=BenchmarkSeeder
docker compose exec app php artisan vendor:publish --tag=async-config

curl http://localhost:8083/hello

Updating the adapter

When yangusik/laravel-spawn is updated, pull the latest version:

cd trueasync
docker compose run --rm app composer update yangusik/laravel-spawn --ignore-platform-reqs
docker compose restart app

DB connection pool

TrueAsync uses a coroutine-aware PDO pool. The pool size is configured in config/async.php (publish with php artisan vendor:publish --tag=async-config):

'db_pool' => [
    'enabled' => true,
    'min'     => 2,
    'max'     => 10,   // per worker — 12 workers × 10 = 120 max connections total
    'healthcheck_interval' => 30,
],

PostgreSQL max_connections is set to 500 in compose.yml to accommodate the pool. Monitor live connections during load:

curl -s http://localhost:8083/debug/connections | jq .

# Or watch during k6 run
watch -n1 'curl -s http://localhost:8083/debug/connections | jq "{total,max_connections,by_state}"'

Running benchmarks

All scripts use the same load: 840 hello req/s + 360 DB req/s = 1200 req/s total.

# PHP-FPM
k6 run k6/fpm.js

# Octane Swoole
k6 run k6/octane_swoole.js

# TrueAsync
k6 run k6/trueasync.js

# /bench endpoint (5 DB queries per request), target adapter via BASE_URL
BASE_URL=http://localhost:8083 k6 run k6/bench.js
BASE_URL=http://localhost:8084 k6 run k6/bench.js
BASE_URL=http://localhost:8082 k6 run k6/bench.js

Results

Load: 840 req/s /hello + 360 req/s /test = 1200 req/s total, constant-arrival-rate, 30s duration. All adapters: 12 workers. Environment: WSL2 (Linux 6.6 on Windows).

Metric PHP-FPM (12w) Octane Swoole (12w) TrueAsync (12w)
Target rate 1200 req/s 1200 req/s 1200 req/s
Actual throughput ~200 req/s ~752 req/s ~1118 req/s
Dropped iterations ~28 000 ~5 000 20
avg latency ~4 000ms ~880ms 13ms
p(95) latency ~5 000ms 2 320ms 21ms
p(95) < 200ms
Failed requests 0% 0% 0%
DB connections (peak) 120

Architecture comparison

PHP-FPM Octane Swoole TrueAsync
Request model Process per request 1 process = 1 request at a time 1 worker = N coroutines
DB I/O Blocking (new conn each req) Blocking (PDO synchronous) Non-blocking (coroutine yield)
Memory model Stateless Long-lived process Long-lived process + coroutine context isolation
App bootstrap Every request Once per worker Once per worker

Why TrueAsync wins on DB-bound load: Swoole keeps the app in memory (avoids bootstrap cost) but PDO is still synchronous — a worker blocked on pg_sleep(0.01) cannot accept another request. TrueAsync yields the coroutine on every DB call, so one worker handles hundreds of concurrent DB-bound requests without blocking.


Notes

  • Each adapter has its own PostgreSQL instance on a separate port to avoid interference
  • APP_DEBUG=false in all setups for fair comparison
  • OPcache enabled in PHP-FPM
  • PostgreSQL max_connections=500 in all setups
  • Absolute numbers will be higher on bare metal (benchmarks run on WSL2)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors