The contenders
| Framework & Source code | Runtime | ORM |
|---|---|---|
| Laravel 12 (api / image) | FrankenPHP 8.5 | Eloquent |
| Symfony 8.0 (api / image) | FrankenPHP 8.5 | Doctrine 3 |
| FastAPI (api / image) | Python 3.14 (asyncio) | SQLAlchemy 2.0 (asyncio) |
| NestJS 11 (api / image) | Node 24 | Prisma 7 |
| Spring Boot 4.0 (api / image) | Java 25 | Hibernate 6 |
| ASP.NET Core 10 (api / image) | .NET 10 | EF Core 10 |
The Swarm cluster for testing
Type of machine used for workers and db storage : cpx32, 4 vCPUs, 8GB RAM.
App deployment configurations
version: "3.8"
services: app: image: gitea.okami101.io/conduit/laravel:latest environment: - APP_KEY=base64:nltxnFb9OaSAr4QcCchy8dG1QXUbc2+2tsXpzN9+ovg= - DB_CONNECTION=pgsql - DB_HOST=postgres_db - DB_USERNAME=okami - DB_PASSWORD=okami - DB_DATABASE=conduit_laravel - JWT_SECRET_KEY=c2b344e1-1a20-47fc-9aef-55b0c0d568a7 networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.laravel.entrypoints=websecure - traefik.http.services.laravel.loadbalancer.server.port=8000 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueversion: "3.8"
services: app: image: gitea.okami101.io/conduit/symfony:latest environment: - SERVER_NAME=:80 - APP_SECRET=ede04f29dd6c8b0e404581d48c36ec73 - DATABASE_DRIVER=pdo_pgsql - DATABASE_URL=postgresql://okami:okami@postgres_db/conduit_symfony - DATABASE_RO_URL=postgresql://okami:okami@postgres_db/conduit_symfony - JWT_PASSPHRASE=c2b344e1-1a20-47fc-9aef-55b0c0d568a7 - FRANKENPHP_CONFIG=worker ./public/index.php networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.symfony.entrypoints=websecure - traefik.http.services.symfony.loadbalancer.server.port=80 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueversion: "3.8"
services: app: image: gitea.okami101.io/conduit/fastapi:latest environment: - DB_HOST=postgres_db - DB_RO_HOST=postgres_db - DB_PORT=5432 - DB_USERNAME=okami - DB_PASSWORD=okami - DB_DATABASE=conduit_fastapi - JWT_PASSPHRASE=c2b344e1-1a20-47fc-9aef-55b0c0d568a7 - UVICORN_WORKERS=4 networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.fastapi.entrypoints=websecure - traefik.http.services.fastapi.loadbalancer.server.port=8000 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueversion: "3.8"
services: app: image: gitea.okami101.io/conduit/nestjs:latest environment: - NODE_ENV=production - DATABASE_URL=postgres://okami:okami@postgres_db/conduit_nestjs - JWT_SECRET=c2b344e1-1a20-47fc-9aef-55b0c0d568a7 networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.nestjs.entrypoints=websecure - traefik.http.services.nestjs.loadbalancer.server.port=3000 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueversion: "3.8"
services: app: image: gitea.okami101.io/conduit/spring-boot:latest environment: - SPRING_PROFILES_ACTIVE=production - DB_HOST=postgres_db - DB_PORT=5432 - DB_RO_HOST=postgres_db - DB_USERNAME=okami - DB_PASSWORD=okami - DB_DATABASE=conduit_springboot - JWT_SECRET_KEY=YzJiMzQ0ZTEtMWEyMC00N2ZjLTlhZWYtNTViMGMwZDU2OGE3 networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.springboot.entrypoints=websecure - traefik.http.services.springboot.loadbalancer.server.port=8080 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueversion: "3.8"
services: app: image: gitea.okami101.io/conduit/aspnet-core:latest environment: - Serilog__MinimumLevel__Default=Warning - ConnectionStrings__DefaultConnection=Host=postgres_db;Username=okami;Password=okami;Database=conduit_aspnetcore; - ConnectionStrings__DefaultRoConnection=Host=postgres_db;Username=okami;Password=okami;Database=conduit_aspnetcore; - Jwt__SecretKey=a49ff4e5-12b9-49ac-9d6b-99ff82041c21 networks: - postgres_db - traefik_public deploy: labels: - traefik.enable=true - traefik.http.routers.aspnetcore.entrypoints=websecure - traefik.http.services.aspnetcore.loadbalancer.server.port=8080 replicas: 2 placement: max_replicas_per_node: 1 constraints: - node.labels.run == true
networks: postgres_db: external: true traefik_public: external: trueThe k6 scenarios
Scenario 1 - Database intensive
import http from "k6/http";import { check } from "k6";
export const options = { scenarios: { articles: { env: { CONDUIT_URL: '<framework_url>' }, duration: '1m', executor: 'constant-arrival-rate', rate: '<rate>', timeUnit: '1s', preAllocatedVUs: 50, maxVUs: 200, }, },};
export default function () { const apiUrl = `https://${__ENV.CONDUIT_URL}/api`;
const limit = 10; let offset = 0;
let articles = []
do { const articlesResponse = http.get(`${apiUrl}/articles?limit=${limit}&offset=${offset}`); check(articlesResponse, { "status is 200": (r) => r.status == 200, });
articles = articlesResponse.json().articles;
offset += limit; } while (articles && articles.length >= limit);}The expected pseudocode SQL queries to build this response:
SELECT * FROM articles LIMIT 10 OFFSET 0;SELECT count(*) FROM articles;SELECT * FROM users WHERE id IN (<articles.author_id...>);SELECT * FROM article_tag WHERE article_id IN (<articles.id...>);SELECT * FROM favorites WHERE article_id IN (<articles.id...>);Scenario 2 - Runtime intensive
import http from "k6/http";import { check } from "k6";
export const options = { scenarios: { articles: { env: { CONDUIT_URL: '<framework_url>' }, duration: '1m', executor: 'constant-arrival-rate', rate: '<rate>', timeUnit: '1s', preAllocatedVUs: 50, maxVUs: 200, }, },};
export default function () { const apiUrl = `https://${__ENV.CONDUIT_URL}.sw.okami101.io/api`;
const limit = 10; let offset = 0;
const tagsResponse = http.get(`${apiUrl}/tags`); check(tagsResponse, { "status is 200": (r) => r.status == 200, });
let articles = []
do { const articlesResponse = http.get(`${apiUrl}/articles?limit=${limit}&offset=${offset}`); check(articlesResponse, { "status is 200": (r) => r.status == 200, });
articles = articlesResponse.json().articles;
for (let i = 0; i < articles.length; i++) { const article = articles[i]; const articleResponse = http.get(`${apiUrl}/articles/${article.slug}`); check(articleResponse, { "status is 200": (r) => r.status == 200, });
const commentsResponse = http.get(`${apiUrl}/articles/${article.slug}/comments`); check(commentsResponse, { "status is 200": (r) => r.status == 200, });
const authorsResponse = http.get(`${apiUrl}/profiles/${article.author.username}`); check(authorsResponse, { "status is 200": (r) => r.status == 200, }); } offset += limit; } while (articles && articles.length >= limit);}The results
Laravel
Laravel scenario 1
Iteration creation rate = 20/s
checks_total.......: 53958 815.756471/schecks_succeeded...: 100.00% 53958 out of 53958checks_failed......: 0.00% 0 out of 53958
✓ status is 200
HTTPhttp_req_duration..............: avg=152.53ms min=4.51ms med=142.8ms max=459.15ms p(90)=304.87ms p(95)=344.66ms { expected_response:true }...: avg=152.53ms min=4.51ms med=142.8ms max=459.15ms p(90)=304.87ms p(95)=344.66mshttp_req_failed................: 0.00% 0 out of 53958http_reqs......................: 53958 815.756471/s
EXECUTIONdropped_iterations.............: 142 2.146807/siteration_duration.............: avg=7.8s min=1.67s med=8.31s max=12.3s p(90)=10.37s p(95)=10.82siterations.....................: 1058 15.995225/svus............................: 14 min=14 max=190vus_max........................: 192 min=50 max=192
NETWORKdata_received..................: 575 MB 8.7 MB/sdata_sent......................: 5.1 MB 77 kB/sLaravel scenario 2
Iteration creation rate = 5/s
checks_total.......: 130227 1446.284124/schecks_succeeded...: 100.00% 130227 out of 130227checks_failed......: 0.00% 0 out of 130227
✓ status is 200
HTTPhttp_req_duration..............: avg=85.13ms min=2.7ms med=69.64ms max=301.09ms p(90)=187.1ms p(95)=217.36ms { expected_response:true }...: avg=85.13ms min=2.7ms med=69.64ms max=301.09ms p(90)=187.1ms p(95)=217.36mshttp_req_failed................: 0.00% 0 out of 130227http_reqs......................: 130227 1446.284124/s
EXECUTIONdropped_iterations.............: 126 1.39934/svus............................: 175 min=5 max=175vus_max........................: 176 min=50 max=176
NETWORKdata_received..................: 299 MB 3.3 MB/sdata_sent......................: 11 MB 123 kB/sSymfony
Symfony scenario 1
Iteration creation rate = 25/s
checks_total.......: 71298 1119.920793/schecks_succeeded...: 100.00% 71298 out of 71298checks_failed......: 0.00% 0 out of 71298
✓ status is 200
HTTPhttp_req_duration..............: avg=86.13ms min=5.77ms med=84.66ms max=246.66ms p(90)=152.69ms p(95)=182.59ms { expected_response:true }...: avg=86.13ms min=5.77ms med=84.66ms max=246.66ms p(90)=152.69ms p(95)=182.59mshttp_req_failed................: 0.00% 0 out of 71298http_reqs......................: 71298 1119.920793/s
EXECUTIONdropped_iterations.............: 103 1.617883/siteration_duration.............: avg=4.41s min=1.3s med=4.52s max=7.62s p(90)=6.31s p(95)=6.49siterations.....................: 1398 21.959231/svus............................: 59 min=25 max=151vus_max........................: 153 min=50 max=153
NETWORKdata_received..................: 642 MB 10 MB/sdata_sent......................: 6.4 MB 101 kB/sSymfony scenario 2
Iteration creation rate = 5/s
checks_total.......: 296749 3296.716429/schecks_succeeded...: 100.00% 296749 out of 296749checks_failed......: 0.00% 0 out of 296749
✓ status is 200
HTTPhttp_req_duration..............: avg=29.34ms min=1.54ms med=25.39ms max=127.11ms p(90)=57.66ms p(95)=64.72ms { expected_response:true }...: avg=29.34ms min=1.54ms med=25.39ms max=127.11ms p(90)=57.66ms p(95)=64.72mshttp_req_failed................: 0.00% 0 out of 296749http_reqs......................: 296749 3296.716429/s
EXECUTIONdropped_iterations.............: 95 1.055397/siteration_duration.............: avg=43.02s min=20.22s med=47.91s max=54.99s p(90)=53.93s p(95)=54.4siterations.....................: 134 1.488666/svus............................: 72 min=5 max=144vus_max........................: 145 min=50 max=145
NETWORKdata_received..................: 527 MB 5.9 MB/sdata_sent......................: 24 MB 262 kB/sFastAPI
FastAPI scenario 1
Iteration creation rate = 30/s
checks_total.......: 86904 1378.310382/schecks_succeeded...: 100.00% 86904 out of 86904checks_failed......: 0.00% 0 out of 86904
✓ status is 200
HTTPhttp_req_duration..............: avg=74.53ms min=4.91ms med=61.65ms max=802.19ms p(90)=118.93ms p(95)=157.88ms { expected_response:true }...: avg=74.53ms min=4.91ms med=61.65ms max=802.19ms p(90)=118.93ms p(95)=157.88mshttp_req_failed................: 0.00% 0 out of 86904http_reqs......................: 86904 1378.310382/s
EXECUTIONdropped_iterations.............: 96 1.522574/siteration_duration.............: avg=3.81s min=2.01s med=3.71s max=6.28s p(90)=4.87s p(95)=5.18siterations.....................: 1704 27.025694/svus............................: 8 min=8 max=143vus_max........................: 146 min=50 max=146
NETWORKdata_received..................: 708 MB 11 MB/sdata_sent......................: 7.8 MB 124 kB/sFastAPI scenario 2
Iteration creation rate = 5/s
checks_total.......: 222475 2471.191074/schecks_succeeded...: 100.00% 222475 out of 222475checks_failed......: 0.00% 0 out of 222475
✓ status is 200
HTTPhttp_req_duration..............: avg=44.78ms min=3.3ms med=36.13ms max=411.67ms p(90)=84.14ms p(95)=113.58ms { expected_response:true }...: avg=44.78ms min=3.3ms med=36.13ms max=411.67ms p(90)=84.14ms p(95)=113.58mshttp_req_failed................: 0.00% 0 out of 222475http_reqs......................: 222475 2471.191074/s
EXECUTIONdropped_iterations.............: 112 1.244065/siteration_duration.............: avg=58.31s min=39.18s med=59.92s max=1m12s p(90)=1m11s p(95)=1m11siterations.....................: 70 0.777541/svus............................: 119 min=5 max=162vus_max........................: 162 min=50 max=162
NETWORKdata_received..................: 446 MB 5.0 MB/sdata_sent......................: 17 MB 186 kB/sNestJS
NestJS scenario 1
Iteration creation rate = 50/s
checks_total.......: 150756 2464.488829/schecks_succeeded...: 100.00% 150756 out of 150756checks_failed......: 0.00% 0 out of 150756
✓ status is 200
HTTPhttp_req_duration..............: avg=29.36ms min=6.07ms med=27.13ms max=166.53ms p(90)=46ms p(95)=52.98ms { expected_response:true }...: avg=29.36ms min=6.07ms med=27.13ms max=166.53ms p(90)=46ms p(95)=52.98mshttp_req_failed................: 0.00% 0 out of 150756http_reqs......................: 150756 2464.488829/s
EXECUTIONdropped_iterations.............: 45 0.735639/siteration_duration.............: avg=1.52s min=1.08s med=1.5s max=2.08s p(90)=1.78s p(95)=1.84siterations.....................: 2956 48.32331/svus............................: 21 min=21 max=90vus_max........................: 93 min=50 max=93
NETWORKdata_received..................: 2.6 GB 42 MB/sdata_sent......................: 14 MB 227 kB/sNestJS scenario 2
Iteration creation rate = 5/s
checks_total.......: 358512 4332.501822/schecks_succeeded...: 99.93% 358281 out of 358512checks_failed......: 0.06% 231 out of 358512
✗ status is 200 ↳ 99% — ✓ 358281 / ✗ 231
HTTPhttp_req_duration..............: avg=17.72ms min=1.51ms med=14.96ms max=122.66ms p(90)=32.44ms p(95)=39.51ms { expected_response:true }...: avg=17.73ms min=1.51ms med=14.96ms max=122.66ms p(90)=32.45ms p(95)=39.51mshttp_req_failed................: 0.06% 231 out of 358512http_reqs......................: 358512 4332.501822/s
EXECUTIONdropped_iterations.............: 69 0.833843/siteration_duration.............: avg=27.72s min=17.94s med=28.6s max=33.76s p(90)=32.74s p(95)=33.13siterations.....................: 231 2.79156/svus............................: 9 min=5 max=119vus_max........................: 119 min=50 max=119
NETWORKdata_received..................: 1.6 GB 20 MB/sdata_sent......................: 29 MB 348 kB/sSpring Boot
Spring Boot scenario 1
Iteration creation rate = 60/s
checks_total.......: 181101 2978.075175/schecks_succeeded...: 100.00% 181101 out of 181101checks_failed......: 0.00% 0 out of 181101
✓ status is 200
HTTPhttp_req_duration..............: avg=21.08ms min=2.28ms med=15.76ms max=185.05ms p(90)=41.1ms p(95)=50.77ms { expected_response:true }...: avg=21.08ms min=2.28ms med=15.76ms max=185.05ms p(90)=41.1ms p(95)=50.77mshttp_req_failed................: 0.00% 0 out of 181101http_reqs......................: 181101 2978.075175/s
EXECUTIONdropped_iterations.............: 50 0.822214/siteration_duration.............: avg=1.1s min=518.89ms med=1.09s max=2.03s p(90)=1.54s p(95)=1.64siterations.....................: 3551 58.393631/svus............................: 97 min=34 max=98vus_max........................: 100 min=55 max=100
NETWORKdata_received..................: 3.4 GB 56 MB/sdata_sent......................: 16 MB 255 kB/sSpring Boot scenario 2
Iteration creation rate = 5/s
checks_total.......: 361616 4497.264215/schecks_succeeded...: 100.00% 361616 out of 361616checks_failed......: 0.00% 0 out of 361616
✓ status is 200
HTTPhttp_req_duration..............: avg=16.59ms min=1.7ms med=13ms max=265.36ms p(90)=32.5ms p(95)=41.83ms{ expected_response:true }...: avg=16.59ms min=1.7ms med=13ms max=265.36ms p(90)=32.5ms p(95)=41.83mshttp_req_failed................: 0.00% 0 out of 361616http_reqs......................: 361616 4497.264215/s
EXECUTIONdropped_iterations.............: 68 0.845687/siteration_duration.............: avg=26s min=12.3s med=27.08s max=33.23s p(90)=32.11s p(95)=32.42siterations.....................: 233 2.897722/svus............................: 6 min=5 max=116vus_max........................: 118 min=50 max=118
NETWORKdata_received..................: 1.6 GB 20 MB/sdata_sent......................: 32 MB 396 kB/sASP.NET Core
ASP.NET Core scenario 1
Iteration creation rate = 45/s
checks_total.......: 137700 2276.867791/schecks_succeeded...: 100.00% 137700 out of 137700checks_failed......: 0.00% 0 out of 137700
✓ status is 200
HTTPhttp_req_duration..............: avg=14.75ms min=3.1ms med=13.93ms max=89.59ms p(90)=21.13ms p(95)=23.82ms { expected_response:true }...: avg=14.75ms min=3.1ms med=13.93ms max=89.59ms p(90)=21.13ms p(95)=23.82mshttp_req_failed................: 0.00% 0 out of 137700http_reqs......................: 137700 2276.867791/s
EXECUTIONiteration_duration.............: avg=780.66ms min=498.88ms med=773.39ms max=1.1s p(90)=888.81ms p(95)=928.54msiterations.....................: 2700 44.644466/svus............................: 30 min=29 max=46vus_max........................: 50 min=50 max=50
NETWORKdata_received..................: 3.1 GB 52 MB/sdata_sent......................: 12 MB 201 kB/sASP.NET Core scenario 2
Iteration creation rate = 5/s
checks_total.......: 383344 5099.848651/schecks_succeeded...: 100.00% 383344 out of 383344checks_failed......: 0.00% 0 out of 383344
✓ status is 200
HTTPhttp_req_duration..............: avg=13.64ms min=1.27ms med=11.96ms max=235.6ms p(90)=23.93ms p(95)=28.28ms { expected_response:true }...: avg=13.64ms min=1.27ms med=11.96ms max=235.6ms p(90)=23.93ms p(95)=28.28mshttp_req_failed................: 0.00% 0 out of 383344http_reqs......................: 383344 5099.848651/s
EXECUTIONdropped_iterations.............: 53 0.70509/siteration_duration.............: avg=21.41s min=15.48s med=21.44s max=25.06s p(90)=24.41s p(95)=24.66siterations.....................: 247 3.285985/svus............................: 2 min=2 max=102vus_max........................: 103 min=50 max=103
NETWORKdata_received..................: 2.3 GB 31 MB/sdata_sent......................: 34 MB 456 kB/sConclusion
Here are the final req/s results for each framework against PgSQL database.