Skip to main content
Aug 31, 2026http3 load testing

By Lucas Yoris · Performate

HTTP/2 and HTTP/3 Under Load: What Changes in Latency, Errors, and TLS Costs

HTTP/2 vs HTTP/3 load testing: multiplexing, QUIC, TLS costs, and honest k6 comparisons—MDN evolution notes and Grafana k6 HTTP metrics.

HTTP/3 shaved 20ms in a lab slide—then production tails worsened because UDP buffers, cert chains, and CDN PoPs were not held constant. Protocol benchmarks become ops archaeology unless you freeze TLS policy, payload sizes, and geography.

HTTP/2 multiplexes many requests over one TCP connection; HTTP/3 moves transport to QUIC (UDP-based) and typically improves head-of-line blocking at the cost of different CPU and handshake profiles (Evolution of HTTP, RFC 9114 QUIC overview for HTTP/3). For API teams, the question is not which RFC logo wins but which stack your edge and clients actually negotiate under production cert and cipher policy. This guide shows how to compare H2 and H3 honestly in k6, which failure modes differ by protocol, and when to escalate tails vs error rates.

Pair experiments with CDN cache behavior and geo-distributed testing when edges terminate stacks per region.

Why protocol changes move tails—not just averages

Under load, differences show up in:

  • Connection setup — QUIC handshakes vs TCP+TLS reuse; cold starts exaggerate first-byte latency.
  • Lossy paths — HTTP/3 can reduce head-of-line blocking on mobile networks; LAN labs may show no win.
  • Middleboxes — UDP filtering causes resets that HTTP/2 never saw on the same path.
  • Server CPU — encryption and QUIC user-space stacks shift cost from kernel wait to CPU burn.

Averages hide those effects. Compare p95/p99 with identical RPS (p95 vs p99) and tag runs proto:h2 vs proto:h3 (tags and groups).

k6 does not replace packet capture. Use load tests to quantify user-visible latency and error budgets under sustained RPS; use pcaps or edge logs when you need to prove middlebox UDP drops or TLS renegotiation storms. The script above is for regression gates, not RFC conformance certification.

When a protocol “win” is a configuration drift

Changing ALPN advertisement without updating kernel udp buffer limits or max_open_files on generators produces fake regressions (fine-tuning OS). Document infra changes beside protocol toggles so reports stay comparable.

Practical k6 implementation: tagged protocol scenarios

k6 records http_req_duration and related timings—compare runs only when certificate chain, TLS version policy, payload sizes, and CDN PoP behavior are frozen.

Example script (illustrative—not production-ready). Endpoints must expose H2 and H3 on your infra; fictional hosts.

What this example demonstrates:

  • Parallel scenarios hitting H2-only and H3-capable base URLs (or host aliases) at the same RPS.
  • Protocol tags for segmented thresholds.
  • Identical payload and headers so compression and auth match.
  • Failure and tail thresholds per protocol, not one aggregate line.
import http from 'k6/http';
import { check, sleep } from 'k6';

const H2_BASE = __ENV.H2_BASE || 'https://h2.staging.example.com';
const H3_BASE = __ENV.H3_BASE || 'https://h3.staging.example.com';
const BODY = JSON.stringify({ query: 'latency-probe', size: 'medium' });

export const options = {
  scenarios: {
    proto_h2: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.TARGET_RPS || 80),
      timeUnit: '1s',
      duration: '8m',
      preAllocatedVUs: 30,
      maxVUs: 120,
      tags: { proto: 'h2' },
      exec: 'hitH2',
    },
    proto_h3: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.TARGET_RPS || 80),
      timeUnit: '1s',
      duration: '8m',
      preAllocatedVUs: 30,
      maxVUs: 120,
      tags: { proto: 'h3' },
      exec: 'hitH3',
    },
  },
  thresholds: {
    'http_req_duration{proto:h2}': ['p(95)<420', 'p(99)<750'],
    'http_req_duration{proto:h3}': ['p(95)<400', 'p(99)<720'],
    'http_req_failed{proto:h2}': ['rate<0.005'],
    'http_req_failed{proto:h3}': ['rate<0.008'],
  },
};

export function hitH2() {
  const res = http.post(`${H2_BASE}/api/search`, BODY, {
    headers: { 'Content-Type': 'application/json' },
    tags: { proto: 'h2', route: 'search' },
  });
  check(res, { 'h2 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.1);
}

export function hitH3() {
  const res = http.post(`${H3_BASE}/api/search`, BODY, {
    headers: { 'Content-Type': 'application/json' },
    tags: { proto: 'h3', route: 'search' },
  });
  check(res, { 'h3 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.1);
}

Patterns that work

  • Baseline the real client mix (HTTP/1.1 upgrades, H2 everywhere, or H3 advertising) before synthetic toggles.
  • Ramp identical RPS/VU models; compare tails, not averages alone.
  • Log negotiation from ingress for the test window; align tags with observed splits.
  • Run mesh on/off pairs in Kubernetes when sidecars terminate TLS differently (Kubernetes microservices).

Anti-patterns to avoid

  • Benchmarking different payload sizes “because H3 is newer.”
  • Ignoring generator UDP tuning while blaming the app for early resets.
  • Declaring victory on LAN staging when mobile loss paths drive prod tails.

Pro tip (example command):

k6 run http-proto-compare.js --summary-trend-stats="p(95),p(99)" -e TARGET_RPS=80

What this command demonstrates: side-by-side percentile trends for proto:h2 and proto:h3 tags in one summary.

Decision framework: when to invest in H3 under load

SignalOften protocol-related whenRecommended action
Rising tail latency, stable CPUBuffering / QUIC loss on pathCapture pcaps; test from lossy link profile
Early connection resetsMiddleboxes mishandling UDPFirewall lab; fallback policy
Shorter lived sessionsAggressive idle timeouts on new pathAlign CDN/proxy keep-alive with H2 baseline
H3 wins on p99 mobile onlyLossy radio pathsGeo + device mix tests (geo-distributed)
H3 higher CPU, similar tailsEncryption/stack costCapacity plan CPU headroom

Stay on H2 if staging shows no tail win and ops cost of UDP path debugging is high.

Pilot H3 when ingress logs show material H3 share and mobile tails dominate SLO breaches.

Block release when http_req_failed{proto:h3} exceeds budget even if p95 looks better—errors trump averages.

Stakeholder narrative

Explain trade-offs using throughput vs latency: HTTP/3 may lower tails for lossy networks while shifting server CPU—budget both. Attach protocol tags to reports (how to read load test reports) so leadership sees the same splits engineering used.

Pre-release checklist

  • Document TLS cert chain, cipher policy, and ALPN settings for H2 vs H3 runs.
  • Freeze payload size, compression, and auth headers across protocol scenarios.
  • Record generator OS tuning (udp buffers, max_open_files) for H3 runs.
  • Compare p95/p99 and http_req_failed per proto tag, not global aggregates.
  • Archive ingress negotiation stats for the test window beside k6 exports.

How Performate simplifies protocol A/B runs

Toggle endpoints without rewriting entire collections each release.

Example: compare H2 and H3 endpoints from one collection

  1. Import search/checkout requests once; duplicate hosts for h2. and h3. aliases. Problem solved: identical bodies and headers across protocols.
  2. Create two scenarios with the same arrival rate and tags proto:h2 / proto:h3. Problem solved: honest RPS matching without script forks.
  3. Run and open comparison view filtered by protocol tag. Problem solved: tails visible per stack in one export.
  4. Attach infra notes (CDN PoP, cert change) in the report footer for the deprecation meeting.
  5. Export k6 for CI regression when H3 share crosses your adoption threshold.
  6. Share with SRE alongside debug failing load test runbook if resets spike mid-test.

Closing takeaway

Protocol comparisons are controlled experiments, not slogan slides. Freeze TLS, payloads, and geography; tag every request; judge tails and errors before averages.

Run your next H2/H3 pair at identical RPS—and record whether p99 or http_req_failed moved first. That answer tells you if the stack is ready, not just faster on paper.

Try Performate free | Book a demo | k6 HTTP requests

Ready to optimize your API performance?

Use Performate to turn this playbook into runnable k6 scenarios, thresholds, and shareable reports without losing days to glue code.

← Back to all posts