Skip to content

Repository files navigation

WebStress

WebStress is a controlled, Go-based HTTP performance and bottleneck analysis tool for systems you own or are explicitly authorized to test. It goes beyond raw request generation: it tells you at which load level degradation begins, whether that degradation appears client-side (generator saturation) or server-side, and which endpoint is responsible.

Use only on systems you own or have explicit written authorization to test. WebStress will not run without the --authorized flag. Public targets can additionally require a served ownership token before load starts.

See README_TR.md for the full Turkish guide and docs/ for architecture, security, and testing notes.


Table of Contents


Features

Category Capability
Load models Open arrival-rate (RPS) and concurrent-user (virtual users)
Stages Constant, linear ramp, multi-stage pipelines
Traffic Weighted endpoint distribution
Sessions Per-worker isolated cookie jars for authenticated flows
Assertions Status code range + body content checks
Variables {{timestamp}}, {{random_int}}, {{random_uuid}}
Timings DNS · TCP connect · TLS · connection wait · TTFB · server wait · total
Histograms p50 / p90 / p95 / p99 / p99.9 — fixed-memory buckets
Error classes timeout · dns · tls · connection_refused · connection_reset · redirect_blocked · unexpected_status · assertion · body_read · body_too_large · canceled
Dropped requests Measures scheduler saturation; separates generator limit from server limit
Safety abort Auto-stop on error rate, timeout rate, p99, or dropped-rate thresholds
Dashboard Live localhost HTML dashboard + JSON API
Reports Self-contained HTML report + raw JSON report
Analysis Breakpoint detection, tail-latency spread, hotspot identification, phase attribution
Prometheus Optional exposition endpoint scrape for CPU/memory/goroutine correlation
Security Authorization flag · host allowlist · loopback-only dashboard · opt-in proxy · secret redaction

Requirements

  • Go 1.23+ (for building from source)
  • Linux, macOS, or Windows (pre-built binaries available in dist/)

Installation

Pre-built binaries

Pre-built binaries are available in the dist/ directory for all major platforms:

File Platform
dist/webstress-linux-amd64 Linux x86-64
dist/webstress-linux-arm64 Linux ARM64 (e.g. Raspberry Pi, AWS Graviton)
dist/webstress-darwin-amd64 macOS Intel
dist/webstress-darwin-arm64 macOS Apple Silicon (M1/M2/M3)
dist/webstress-windows-amd64.exe Windows x86-64

Linux

# x86-64
cp dist/webstress-linux-amd64 /usr/local/bin/webstress
chmod +x /usr/local/bin/webstress

# ARM64
cp dist/webstress-linux-arm64 /usr/local/bin/webstress
chmod +x /usr/local/bin/webstress

Verify:

webstress version
webstress about

macOS

# Apple Silicon (M1/M2/M3)
cp dist/webstress-darwin-arm64 /usr/local/bin/webstress
chmod +x /usr/local/bin/webstress

# Intel
cp dist/webstress-darwin-amd64 /usr/local/bin/webstress
chmod +x /usr/local/bin/webstress

macOS may show a Gatekeeper warning the first time you run the binary. To allow it: System Settings → Privacy & Security → Allow Anyway, or run:

xattr -dr com.apple.quarantine /usr/local/bin/webstress

Verify:

webstress version
webstress about

Windows

Copy dist/webstress-windows-amd64.exe to a directory that is on your PATH (for example C:\Tools\) and rename it to webstress.exe for convenience:

# PowerShell
Copy-Item dist\webstress-windows-amd64.exe C:\Tools\webstress.exe

Or run it directly without installing:

.\dist\webstress-windows-amd64.exe version
.\dist\webstress-windows-amd64.exe about

Run a test on Windows:

.\dist\webstress-windows-amd64.exe validate -config examples\smoke-local.json
.\dist\webstress-windows-amd64.exe run -config examples\smoke-local.json --authorized

The live dashboard is available at http://127.0.0.1:8787 on Windows as well. Reports are written to webstress-reports\ in the current directory.

Verify checksums

Before using a binary, verify its integrity with the provided SHA256 checksums:

# Linux / macOS
sha256sum -c dist/SHA256SUMS

# macOS (if sha256sum is not available, use shasum)
shasum -a 256 -c dist/SHA256SUMS
# Windows PowerShell — verify a single binary
Get-FileHash dist\webstress-windows-amd64.exe -Algorithm SHA256
# Compare the output hash against the value in dist/SHA256SUMS

Build from source

git clone https://github.com/cumakurt/webstress.git
cd webstress
make build
# binary → dist/webstress

Cross-compile all platform targets at once:

make release
# produces all binaries in dist/

Verify the build:

./dist/webstress version
./dist/webstress about

Quick Start

Linux / macOS

1. Validate your config first:

./dist/webstress validate -config examples/smoke-local.json

2. Run against a local target:

./dist/webstress run -config examples/smoke-local.json --authorized

3. Open the live dashboard (while the test is running):

http://127.0.0.1:8787

4. Check the reports after the test finishes:

webstress-reports/smoke-YYYYMMDD-HHMMSS.html
webstress-reports/smoke-YYYYMMDD-HHMMSS.json

Windows (PowerShell)

1. Validate your config:

.\dist\webstress-windows-amd64.exe validate -config examples\smoke-local.json

2. Run a test:

.\dist\webstress-windows-amd64.exe run -config examples\smoke-local.json --authorized

3. Open the live dashboard (while the test is running):

http://127.0.0.1:8787

4. Check the reports:

webstress-reports\smoke-YYYYMMDD-HHMMSS.html
webstress-reports\smoke-YYYYMMDD-HHMMSS.json

If you copied the binary to C:\Tools\webstress.exe and added C:\Tools to your PATH, you can simply use webstress instead of the full binary path in all commands above.


Load Models

Arrival-Rate Model

The scheduler drives a target RPS regardless of how long responses take. This prevents the closed-loop effect where a slow server naturally receives fewer requests and the bottleneck is hidden.

Constant stage:

{"name": "baseline", "duration": "30s", "rps": 100}

Linear ramp:

{"name": "ramp", "duration": "60s", "rps_start": 100, "rps_end": 1000}

Full example:

"load": {
  "model": "arrival-rate",
  "stages": [
    {"name": "warmup",   "duration": "20s",  "rps": 25},
    {"name": "ramp",     "duration": "90s",  "rps_start": 50, "rps_end": 1000},
    {"name": "hold",     "duration": "60s",  "rps": 1000},
    {"name": "cooldown", "duration": "15s",  "rps": 25}
  ]
}

When the scheduler cannot dispatch a request because max_concurrency is exhausted, it records the request as dropped instead of silently queuing it. A rising dropped-rate means the load generator — not the server — is saturated at that concurrency ceiling.

Concurrency Model

Each worker acts as an independent virtual user. Workers run continuously (with optional think time) for the stage duration.

"load": {
  "model": "concurrency",
  "concurrency": 20,
  "think_time_ms": 250,
  "stages": [
    {"name": "warmup", "duration": "20s", "concurrency": 5},
    {"name": "normal", "duration": "60s", "concurrency": 20},
    {"name": "stress", "duration": "60s", "concurrency": 50}
  ]
}

Session Flows

Flows model real authenticated user journeys. Each virtual user gets its own cookie jar — cookies set in step N are available in step N+1.

"flows": [
  {
    "name": "login-and-browse",
    "weight": 1,
    "think_time_ms": 150,
    "continue_on_error": false,
    "steps": [
      {
        "name": "login",
        "method": "POST",
        "path": "/login",
        "body": "{\"username\":\"loaduser\",\"password\":\"REPLACE_ME\"}",
        "expect": {"status_min": 200, "status_max": 299}
      },
      {
        "name": "profile",
        "method": "GET",
        "path": "/profile",
        "expect": {"status_min": 200, "status_max": 299}
      },
      {
        "name": "item",
        "method": "GET",
        "path": "/items/{{random_int}}",
        "pause_ms": 300,
        "expect": {"status_min": 200, "status_max": 404}
      }
    ]
  }
]

traffic and flows cannot be used together in the same config. Flows require the concurrency load model to preserve session semantics.


Assertions

Each endpoint or flow step can declare expected response behaviour:

"expect": {
  "status_min": 200,
  "status_max": 299,
  "contains": "expected text"
}

A request that receives a transport-level response but fails an assertion is counted as an error (unexpected_status or assertion class). This prevents passing load tests that silently return 500s.


Dynamic Variables

Use these placeholders in path, body, and request headers:

Variable Generates
{{timestamp}} Current Unix timestamp (seconds)
{{random_int}} Random non-negative integer
{{random_uuid}} RFC 4122 UUID v4

Example:

{"name": "search", "method": "GET", "path": "/api/items/{{random_int}}?ts={{timestamp}}"}

Safety Limits

Hard ceilings that stage values cannot exceed:

"limits": {
  "max_rps": 2500,
  "max_concurrency": 2000,
  "max_body_bytes": 2097152
}
  • max_rps — maximum requests per second across all stages
  • max_concurrency — maximum simultaneous in-flight requests; excess arrivals are counted as dropped
  • max_body_bytes — response bodies larger than this are truncated and counted as body_too_large

Automatic Abort

WebStress can abort the test automatically when sustained degradation is detected:

"abort": {
  "error_rate":          0.05,
  "timeout_rate":        0.02,
  "p99_ms":              5000,
  "dropped_rate":        0.05,
  "consecutive_windows": 3,
  "min_samples":         50
}
Field Meaning
error_rate Fraction of requests ending in any error class
timeout_rate Fraction of requests ending in timeout specifically
p99_ms p99 latency ceiling in milliseconds
dropped_rate Fraction of scheduled requests that were dropped
consecutive_windows Number of consecutive 1-second windows that must all breach a threshold before aborting — prevents single-spike false positives
min_samples Minimum requests in a window before abort logic activates

HTTP Timing Breakdown

WebStress captures the full Go httptrace lifecycle for every request:

Phase What it measures
DNS Name resolution duration
TCP connect Socket establishment
TLS TLS handshake
Connection wait Time the client waited to acquire a connection from the pool
TTFB Total time from request start to first response byte received
Server wait Time from last request byte written to first response byte — the closest client-visible proxy for backend processing time
Total Time to read the full response body (up to max_body_bytes)

The analyzer uses these phases to produce directional hints:

  • High server wait / total ratio → likely backend processing bottleneck
  • High connection wait → connection pool pressure or connection exhaustion
  • High DNS or TLS → infrastructure-level latency
  • Low connection reuse ratio → keep-alive issues or frequent reconnects

These are probability-weighted hints, not root-cause proof. Validate with Prometheus metrics or distributed tracing.


Live Dashboard

While a test is running, a dashboard is available at http://127.0.0.1:8787 (or the address set in live.listen).

The dashboard shows in real time:

  • Active stage name and target RPS
  • Achieved RPS
  • p50 / p95 / p99 latency
  • Error rate and timeout rate
  • Dropped request count
  • Per-endpoint request count, error count, p95, p99
  • Load timeline chart

The JSON API endpoint (/api/status) returns the same data in machine-readable form.

Security: the dashboard can only bind to localhost / loopback addresses. Binding to a public interface is rejected at startup.

Configure:

"live": {
  "enabled": true,
  "listen": "127.0.0.1:8787"
}

Reports

After the test completes, two files are written to the report directory:

webstress-reports/<prefix>-YYYYMMDD-HHMMSS.html
webstress-reports/<prefix>-YYYYMMDD-HHMMSS.json

HTML Report

Self-contained — no external CDN or JavaScript dependencies. Suitable for archiving and sharing. Contains:

  • Total request count and average throughput
  • Error rate, timeout rate
  • p50 / p95 / p99 / max latency
  • Load curve (RPS over time)
  • Observed sustainable and breakpoint RPS estimates
  • Analyzer findings with directional hints
  • HTTP phase averages (DNS, TCP, TLS, connection wait, server wait, total)
  • Per-endpoint summary table
  • Safety preflight and abort details

JSON Report

Raw structured output suitable for CI comparison, diff tooling, or post-hoc analysis. Includes the full timeline, per-second buckets, Prometheus metric snapshots (if enabled), and all analyzer findings.

Configure:

"report": {
  "directory": "./webstress-reports",
  "prefix": "staging-ramp"
}

Public Target Ownership Verification

To prevent accidental or unauthorized use against public systems, WebStress can require a served token at a well-known path before load begins.

Config:

"verification": {
  "required_for_public": true,
  "path": "/.well-known/webstress-verify.txt",
  "token": "REPLACE_WITH_A_LONG_RANDOM_TOKEN"
}

The target web server must return the exact same token as plain text at:

https://your-target.example.com/.well-known/webstress-verify.txt

If the token does not match, load does not start. The token is redacted from all reports.


Prometheus Correlation

WebStress can scrape a Prometheus exposition endpoint and embed the time series in the JSON report alongside load data:

"telemetry": {
  "prometheus_url": "http://127.0.0.1:9100/metrics",
  "interval": "2s",
  "metrics": {
    "process_cpu":    "process_cpu_seconds_total",
    "process_memory": "process_resident_memory_bytes",
    "go_goroutines":  "go_goroutines"
  }
}

If a metric has multiple label combinations (e.g. per-CPU), their values are summed. Know the semantics of the metrics you select.

This enables correlating load curves with CPU saturation, memory growth, or goroutine leaks without needing a separate Grafana setup for the test run.


Redirect Safety

HTTP redirects are followed only to hosts listed in target.allowed_hosts. A redirect to any other host results in a redirect_blocked error and the request is not followed. This prevents a test from accidentally driving load to a third-party domain.

"target": {
  "base_url": "https://staging.example.com",
  "allowed_hosts": ["staging.example.com"]
}

Proxy Behaviour

HTTP_PROXY and HTTPS_PROXY environment variables are ignored by default. Test traffic is sent directly to the target.

To intentionally route through a proxy (only when you are also authorized to load the proxy):

"target": {
  "use_proxy_env": true
}

TLS

TLS certificate verification is enabled by default. Only disable it for internal test environments with self-signed certificates:

"target": {
  "insecure_tls": true
}

Never use insecure_tls: true against production-like targets.


Secret Redaction

Before writing reports, WebStress redacts:

  • Authorization header values
  • Cookie header values
  • Any header whose name contains token, secret, api-key, or apikey
  • The ownership verification token
  • Request body content

Store reports in a trusted location. Avoid embedding sensitive data in URL paths or query strings, as those are not redacted.


Error Classes

Class Cause
timeout Request exceeded timeout_ms
dns DNS resolution failure
tls TLS handshake failure
connection_refused Target actively refused the connection
connection_reset Connection reset by peer mid-request
redirect_blocked Redirect target not in allowed_hosts
unexpected_status Response status outside expect.status_min/status_max
assertion Response body did not match expect.contains
body_read Error reading response body
body_too_large Response body exceeded max_body_bytes
canceled Request canceled (e.g. due to safety abort)

Generator Saturation vs Server Bottleneck

One of WebStress's key design goals is distinguishing between the load generator being the bottleneck and the server being the bottleneck.

  • In arrival-rate mode, if in-flight requests pile up and hit max_concurrency, new scheduled requests are counted as dropped rather than queued silently.
  • A rising dropped_rate means the load generator itself cannot sustain the target RPS at the current concurrency ceiling — the server result at that point is inconclusive.
  • Remedy: increase max_concurrency, distribute load across multiple generator instances, or reduce target RPS.

The analyzer reports this explicitly so you don't misinterpret generator saturation as a server capacity finding.


Design Limits

WebStress produces strong signals from the client's perspective and optionally from Prometheus metrics. It cannot see:

  • SQL query plans or lock contention inside the application
  • Individual downstream service call latencies
  • Specific garbage collection pauses
  • Internal queue depths

Analyzer findings are "evidence-weighted probabilities" — not definitive root causes. Confirm with distributed tracing or application-level observability.


CI / CD Integration

The JSON report exit code is non-zero when the test aborts due to safety thresholds. Use this in CI to gate deployments:

# GitHub Actions example
- name: Run load test
  run: ./dist/webstress run -config examples/smoke-local.json --authorized
  
- name: Check report
  run: |
    REPORT=$(ls webstress-reports/*.json | tail -1)
    python3 -c "
    import json, sys
    r = json.load(open('$REPORT'))
    assert r['summary']['error_rate'] < 0.01, 'error rate too high'
    assert r['summary']['p99_ms'] < 500, 'p99 too high'
    print('Load test passed')
    "

For structured diffing between runs, compare the JSON report summary object across commits.


Example Configs

File Description
examples/smoke-local.json Minimal 5-second smoke test against localhost
examples/ramp-public.json Multi-stage arrival-rate ramp with ownership verification and Prometheus scraping
examples/session-flow.json Concurrency model with authenticated session flow (login → profile → browse)

Build & Test

make test    # unit tests
make race    # race detector
make vet     # static analysis
make build   # compile → dist/webstress

Cross-platform binaries:

make release  # builds all platform targets into dist/

Validate a config without running a test:

./dist/webstress validate -config examples/smoke-local.json

Developer

Display embedded binary metadata:

./dist/webstress about

License

WebStress is licensed under the GNU General Public License v3.0 only (GPL-3.0-only).
See LICENSE for the complete license text.

About

WebStress is a controlled Go-based performance and bottleneck test tool for systems you own or are explicitly authorized to test.

Topics

Resources

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages