Skip to Content
GuideHTTP Performance

HTTP Performance and Load Behavior

The headline throughput number is in the Comparison table (CI-measured, release build, firewall limits raised for the test). This page describes how 3va behaves at its shipped default limits, which is a deliberate design choice, not a performance regression — a single-machine load test against defaults will see most requests rejected with 403/429, and that’s intentional. vvva_firewall operates in the connection-accept loop, before any byte reaches JavaScript, so the runtime never processes a request that exceeds a safety limit.

Default limits

FieldDefaultPurpose
rateLimitRps100 req/s per IPtoken-bucket sustained rate
rateLimitBurst200burst capacity before throttling kicks in
maxConnectionsPerIp50simultaneous connections per IP
maxConnectionsTotal10,000simultaneous connections, all IPs
headerTimeoutMs10,000deadline to receive full headers (Slowloris)
bodyTimeoutMs30,000deadline to receive the full body
minBodyRateBps100 B/sminimum sustained body receive rate (RUDY)
maxHeaderCount100max headers per request
maxHeaderBytes16 KiBmax combined header size
autoBlockThreshold10 violationsviolations before auto-block
blockDurationSecs300 sbase auto-block duration
blockEscalationFactor2multiplier per repeat offense
maxBlockDurationSecs3,600 scap on escalated block duration
strikeDecaySecs3,600 scalm period before strike history resets

At high concurrency, the connection limiter deliberately sheds excess connections rather than queuing them indefinitely, trading raw throughput for protection against overload.

Slowloris and RUDY protection

  • Slowloris (headers trickled in one line per second) — headerTimeoutMs gives every read_line an independent deadline.
  • RUDY (R-U-Dead-Yet — an extremely slow POST body) — bodyTimeoutMs is the total body-read deadline, and minBodyRateBps is a minimum average receive rate: a body arriving below the floor is aborted after ~2s of grace, without waiting for the full 30s deadline.

Adaptive auto-block escalation

autoBlockThreshold + blockDurationSecs are the base layer: repeated violations trigger an automatic block of fixed duration. On top of that, the firewall is adaptive — each auto-block records a strike per IP, and the block duration escalates with reincidence:

auto-block #n → duration = min(blockDurationSecs × blockEscalationFactor^(n-1), maxBlockDurationSecs)

With defaults (base 300s, factor ×2, cap 3600s): 1st offense → 300s, 2nd → 600s, 3rd → 1200s, 4th → 2400s, 5th → 3600s (capped). Strike history is not cleared when a block expires — a repeat attacker gets the next tier. It’s only cleared once the IP goes strikeDecaySecs without another auto-block. This escalation penalizes the block duration for repeat offenders — it does not touch the per-IP token bucket rate itself.

Adaptive rate limiting (EWMA baseline)

A separate, opt-in mechanism (adaptiveRateLimit: true) solves the inverse problem: legitimate traffic spikes. With a fixed threshold, an IP whose legitimate traffic grows gradually (a heavy client, a corporate proxy) crosses rateLimitRps and accumulates violations even though nothing malicious is happening.

window: 1s baseline: ewma = α × count_window + (1 − α) × ewma_prev (α = ewmaAlphaPct / 100, default 20 → α = 0.20) effective rps: max(rateLimitRps, ceil(ewma × 1.5)), capped at rateLimitRps × 4.0

The token bucket consumes against the effective rate, so a high-baseline IP gets more tokens/second without its burst changing. If the IP returns to low traffic, the EWMA decays back down over subsequent windows. An attacker starting from zero has no observed history, so ewma = 0 and the threshold stays static. Disabled by default.

Trusted reverse proxies

trustedProxies (empty by default) lists proxy IPs/CIDRs allowed to supply a client IP via X-Forwarded-For. Connection tracking is per-source-IP — behind an untrusted or unconfigured reverse proxy that collapses many clients onto one IP, the proxy’s own IP is what gets limited.

Request smuggling guard

The request parser supports Transfer-Encoding: chunked and rejects any request carrying both Content-Length and Transfer-Encoding with 400.

Configuring limits

3va.config.ts
export default { firewall: { enabled: true, rateLimitRps: 100, rateLimitBurst: 200, trustedProxies: [], autoBlockThreshold: 10, blockDurationSecs: 300, blockEscalationFactor: 2, maxBlockDurationSecs: 3600, strikeDecaySecs: 3600, maxConnectionsPerIp: 50, maxConnectionsTotal: 10_000, headerTimeoutMs: 10_000, bodyTimeoutMs: 30_000, minBodyRateBps: 100, maxHeaderCount: 100, maxHeaderBytes: 16_384 } }