Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Benchmark: Qail vs GraphQL vs REST Patterns

This benchmark measures one endpoint objective executed with different data-access patterns:

  • Single query with server-side JOIN
  • Batched lookup pattern (DataLoader style)
  • Naive N+1 resolver/call pattern

All approaches must return the same canonical payload shape before timing: { id, name, origin_harbor, dest_harbor }.

Workload

  • Dataset: current swb_staging_local PostgreSQL schema
  • Primary table: odyssey_connections
  • Related table: harbors
  • Query shape for single-query variants: 2x LEFT JOIN
  • Filter: odyssey_connections.is_enabled = true
  • Sort: odyssey_connections.name ASC
  • Limit: 50

Methodology

  • Build profile: --release
  • Iterations: 200
  • Per-approach warmup: 15
  • Global warmup: 15
  • Execution order: randomized each run
  • RLS: bypassed uniformly (SET app.is_super_admin = 'true') to isolate pattern cost
  • Correctness guard: benchmark aborts if payload equivalence check fails
  • Optional RTT injection: BATTLE_SIMULATED_RTT_US (applied per query dispatch in harness)
  • Stats reported: median, p95, avg (avg in raw log), query-count/request

Results (Snapshot: March 25, 2026)

Loopback (BATTLE_SIMULATED_RTT_US=0)

ApproachMedianp95DB Queries / request
GraphQL + DataLoader146.8us168.0us2
Qail AST (uncached)146.9us163.7us1
REST + ?expand=164.5us186.3us1
Qail AST (prepared)347.6us372.0us1
REST naive (N+1 + JSON)4.71ms4.86ms101
GraphQL naive (N+1)4.74ms4.88ms101

Simulated RTT (BATTLE_SIMULATED_RTT_US=250)

ApproachMedianp95DB Queries / request
Qail AST (uncached)475.4us491.2us1
REST + ?expand=486.4us499.9us1
Qail AST (prepared)660.2us688.5us1
GraphQL + DataLoader779.4us802.5us2
REST naive (N+1 + JSON)35.00ms35.39ms101
GraphQL naive (N+1)35.13ms35.52ms101

Simulated RTT (BATTLE_SIMULATED_RTT_US=1000)

ApproachMedianp95DB Queries / request
Qail AST (uncached)1237.8us1252.5us1
REST + ?expand=1248.4us1262.5us1
Qail AST (prepared)1414.6us1448.4us1
GraphQL + DataLoader2287.0us2507.0us2
REST naive (N+1 + JSON)111.46ms112.40ms101
GraphQL naive (N+1)111.56ms112.19ms101

Technical Takeaways

  • The dominant cost shift is query fan-out, not framework branding.
  • On loopback, single-query and DataLoader are effectively tied.
  • As RTT rises, single-query patterns pull ahead because they pay one round trip.
  • Naive N+1 inflates round trips from 1-2 to 101, producing order-of-magnitude latency growth.
  • Prepared vs uncached remains workload-sensitive in this harness and query shape.

Reproduce

DATABASE_URL="postgresql://orion@localhost:5432/swb_staging_local?sslmode=disable" \
BATTLE_ITERATIONS=200 \
BATTLE_WARMUP=15 \
BATTLE_GLOBAL_WARMUP=15 \
BATTLE_SIMULATED_RTT_US=1000 \
cargo run -p qail-pg --example battle_comparison \
  --features chrono,uuid,legacy-raw-examples --release