{"id":46995854,"url":"https://github.com/kurok/pywrkr","last_synced_at":"2026-05-18T20:01:27.022Z","repository":{"id":343679140,"uuid":"1178704764","full_name":"kurok/pywrkr","owner":"kurok","description":"A fast, async Python HTTP benchmarking tool inspired by wrk and Apache ab. Supports concurrent connections, latency breakdown,      percentile stats, SLO thresholds, rate limiting, scripted scenarios, live TUI dashboard, multi-URL testing, distributed master/worker   mode, and observability export (OpenTelemetry, Prometheus).","archived":false,"fork":false,"pushed_at":"2026-05-02T17:09:40.000Z","size":904,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-02T18:32:20.799Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/kurok/pywrkr","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kurok.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":"GOVERNANCE.md","roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["kurok"]}},"created_at":"2026-03-11T09:32:11.000Z","updated_at":"2026-05-02T17:09:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kurok/pywrkr","commit_stats":null,"previous_names":["kurok/pywrk"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/kurok/pywrkr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurok%2Fpywrkr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurok%2Fpywrkr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurok%2Fpywrkr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurok%2Fpywrkr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kurok","download_url":"https://codeload.github.com/kurok/pywrkr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kurok%2Fpywrkr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33189279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-18T09:27:30.708Z","status":"ssl_error","status_checked_at":"2026-05-18T09:27:28.300Z","response_time":71,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-11T15:13:49.963Z","updated_at":"2026-05-18T20:01:27.007Z","avatar_url":"https://github.com/kurok.png","language":"Python","funding_links":["https://github.com/sponsors/kurok"],"categories":["Load Testing"],"sub_categories":[],"readme":"# pywrkr\n\n[![CI](https://github.com/kurok/pywrkr/actions/workflows/python-package.yml/badge.svg)](https://github.com/kurok/pywrkr/actions/workflows/python-package.yml)\n[![CodeQL](https://github.com/kurok/pywrkr/actions/workflows/codeql.yml/badge.svg)](https://github.com/kurok/pywrkr/actions/workflows/codeql.yml)\n[![PyPI version](https://img.shields.io/pypi/v/pywrkr)](https://pypi.org/project/pywrkr/)\n[![Python versions](https://img.shields.io/pypi/pyversions/pywrkr)](https://pypi.org/project/pywrkr/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![codecov](https://codecov.io/gh/kurok/pywrkr/graph/badge.svg)](https://codecov.io/gh/kurok/pywrkr)\n\nA Python HTTP benchmarking tool inspired by [wrk](https://github.com/wg/wrk) and [Apache ab](https://httpd.apache.org/docs/current/programs/ab.html), with extended statistics and virtual user simulation.\n\n## Features\n\n- **HAR import** (`har-import`): convert browser-recorded HAR files into pywrkr scenarios or URL lists — dramatically cuts test authoring time\n- **Five benchmarking modes:**\n  - **Duration mode** (`-d`): wrk-style, run for N seconds\n  - **Request-count mode** (`-n`): ab-style, send exactly N requests\n  - **User simulation mode** (`-u`): simulate virtual users with ramp-up and think time\n  - **Rate limiting mode** (`--rate`): send requests at a controlled, constant rate (with optional ramp)\n  - **Traffic profiles** (`--traffic-profile`): realistic traffic shaping — sine waves, spikes, step functions, business-hour curves, and CSV replay\n  - **Autofind mode** (`--autofind`): automatically ramp load to find maximum sustainable capacity\n- **Detailed latency statistics:** min/max/mean/median/stdev, percentiles (p50-p99.99), histogram, and ab-style \"percentage served within\" table\n- **Throughput timeline:** requests/sec over time in ASCII bar chart\n- **Multiple output formats:** terminal, CSV (`-e`), JSON (`--json`), HTML (`-w`)\n- **HTTP features:** keep-alive toggle, Basic auth (`-A`), cookies (`-C`), custom headers (`-H`), POST body (`-b`/`-p`), content-length verification (`-l`)\n- **Cache-busting** (`-R`): append a unique random query parameter to each request URL\n- **Graceful shutdown:** handles SIGINT/SIGTERM cleanly\n- **Live progress display** with requests/sec, error count, and active user count\n- **SLO-aware thresholds** (`--threshold`): pass/fail criteria like `p95 \u003c 300ms`, `error_rate \u003c 1%` with non-zero exit code on breach — CI-ready\n- **Native observability export:** OpenTelemetry (`--otel-endpoint`) and Prometheus remote write (`--prom-remote-write`)\n- **Test metadata tags** (`--tag`): attach environment, build, region labels to metrics and JSON output\n\n### HAR / Browser-Recording Import\n\nConvert browser-recorded [HAR files](http://www.softwareishard.com/blog/har-12-spec/) (from Chrome DevTools, Firefox, Charles Proxy, Fiddler, etc.) into pywrkr scenarios or URL lists. Similar to k6's HAR converter and JMeter's HTTP(S) Test Script Recorder.\n\n```bash\n# Convert HAR to a pywrkr scenario (JSON):\npywrkr har-import recording.har -o scenario.json\n\n# Then run the generated scenario (URLs come from the scenario):\npywrkr --scenario scenario.json -u 100 -d 60\n\n# Or convert to a URL file for --url-file mode:\npywrkr har-import recording.har --format url-file -o urls.txt\npywrkr --url-file urls.txt -c 50 -d 30\n```\n\n**Recording a HAR file:**\n\n1. Open Chrome DevTools (F12) → Network tab\n2. Navigate through your application\n3. Right-click the network log → \"Save all as HAR with content\"\n\n**Filtering options:**\n\n```bash\n# Only include requests to specific domain(s):\npywrkr har-import recording.har --domain api.example.com -o scenario.json\n\n# Include static assets (CSS, JS, images — excluded by default):\npywrkr har-import recording.har --include-static -o scenario.json\n\n# Exclude analytics/tracking URLs:\npywrkr har-import recording.har --exclude '/analytics' --exclude '/tracking' -o scenario.json\n\n# Only include specific URL patterns:\npywrkr har-import recording.har --include '/api/v2' -o scenario.json\n\n# Preserve original request headers (default: only Content-Type):\npywrkr har-import recording.har --preserve-headers -o scenario.json\n\n# Add status code assertions from recorded responses:\npywrkr har-import recording.har --assert-status -o scenario.json\n\n# Adjust think time (inter-request delay derived from recording):\npywrkr har-import recording.har --think-time-multiplier 0.5 -o scenario.json   # 2x faster\npywrkr har-import recording.har --no-think-time -o scenario.json               # no delays\n```\n\n**HAR import options:**\n\n| Flag | Description |\n|------|-------------|\n| `har_file` | Path to the HAR file (positional, required) |\n| `-o` / `--output` | Output file path (default: print to stdout) |\n| `--format` | Output format: `scenario` (default) or `url-file` |\n| `--name` | Scenario name (default: derived from filename) |\n| `--include-static` | Include static assets (CSS, JS, images, fonts) |\n| `--domain` | Only include requests to this domain (repeatable) |\n| `--exclude` | Exclude URLs matching regex pattern (repeatable) |\n| `--include` | Only include URLs matching regex pattern (repeatable) |\n| `--preserve-headers` | Keep original request headers |\n| `--no-think-time` | Don't derive think times from recorded timing |\n| `--think-time-multiplier` | Scale derived think times (default: 1.0) |\n| `--assert-status` | Assert recorded 2xx/3xx status codes |\n\n## Requirements\n\n- Python 3.10+\n\n```bash\npip install pywrkr\n```\n\n## Quick Start\n\n```bash\n# Basic 10-second benchmark with 10 connections\npywrkr http://localhost:8080/\n\n# 30 seconds, 200 concurrent connections\npywrkr -c 200 -d 30 http://localhost:8080/api\n\n# Send exactly 1000 requests with 50 connections (ab-style)\npywrkr -n 1000 -c 50 http://localhost:8080/\n\n# Simulate 1500 users for 5 minutes with 30s ramp-up and 1s think time\npywrkr -u 1500 -d 300 --ramp-up 30 --think-time 1.0 http://localhost:8080/\n\n# Cache-busting mode (bypass HTTP caches with random query param)\npywrkr -R -c 100 -d 10 http://localhost:8080/\n\n# Constant rate: 500 requests/sec for 30 seconds\npywrkr --rate 500 -d 30 http://localhost:8080/\n\n# Rate ramp: linearly increase from 100 to 1000 req/s over 60 seconds\npywrkr --rate 100 --rate-ramp 1000 -d 60 http://localhost:8080/\n\n# Traffic profiles: sine wave oscillating up to 500 req/s\npywrkr --rate 500 -d 120 --traffic-profile sine http://localhost:8080/\n\n# Traffic profiles: periodic spikes at 5x baseline\npywrkr --rate 200 -d 60 --traffic-profile \"spike:interval=10,multiplier=5\" http://localhost:8080/\n\n# Traffic profiles: replay production traffic from CSV\npywrkr --rate 1000 -d 300 --traffic-profile \"csv:traffic.csv\" http://localhost:8080/\n\n# Autofind: automatically find max sustainable load\npywrkr --autofind --max-error-rate 1 --max-p95 5.0 http://localhost:8080/\n\n# SLO thresholds: exit code 2 if any threshold breached (CI-friendly)\npywrkr --threshold \"p95 \u003c 300ms\" --threshold \"error_rate \u003c 1%\" \\\n    -c 100 -d 30 http://localhost:8080/\n\n# Export metrics to OpenTelemetry collector\npywrkr --otel-endpoint http://localhost:4318 \\\n    --tag environment=staging --tag build=v1.2.3 \\\n    -c 100 -d 30 http://localhost:8080/\n\n# Push metrics to Prometheus Pushgateway\npywrkr --prom-remote-write http://pushgateway:9091 \\\n    --tag region=us-east-1 --tag service=api \\\n    -c 100 -d 30 http://localhost:8080/\n\n# POST with auth, cookies, and JSON output\npywrkr -n 500 -c 20 -m POST -b '{\"key\":\"val\"}' \\\n    -H \"Content-Type: application/json\" \\\n    -A user:pass -C \"session=abc123\" \\\n    --json results.json http://localhost:8080/api\n```\n\n## Usage\n\n```\nusage: pywrkr [-h] [-c CONNECTIONS] [-d DURATION] [-n NUM_REQUESTS]\n              [-t THREADS] [-m METHOD] [-H NAME:VALUE] [-b BODY]\n              [-p POST_FILE] [-A user:pass] [-C COOKIE] [-k]\n              [--no-keepalive] [-l] [-v VERBOSITY] [--timeout TIMEOUT]\n              [--ssl-verify] [--ca-bundle FILE] [-R] [-e FILE] [-w]\n              [--json FILE] [--html-report FILE] [--live]\n              [--latency-breakdown] [--tag TAGS] [--otel-endpoint URL]\n              [--prom-remote-write URL] [--threshold THRESHOLDS]\n              [-u USERS] [--ramp-up RAMP_UP] [--think-time THINK_TIME]\n              [--think-jitter THINK_JITTER] [--rate RATE]\n              [--rate-ramp RATE_RAMP] [--traffic-profile PROFILE]\n              [--scenario FILE] [--autofind]\n              [--max-error-rate MAX_ERROR_RATE] [--max-p95 MAX_P95]\n              [--step-duration STEP_DURATION] [--start-users START_USERS]\n              [--max-users MAX_USERS] [--step-multiplier STEP_MULTIPLIER]\n              [--url-file FILE] [--master] [--worker HOST:PORT]\n              [--expect-workers N] [--bind ADDR] [--port PORT]\n              [url]\n```\n\n### Options\n\n| Flag | Long | Description |\n|------|------|-------------|\n| `url` | | Target URL to benchmark (required) |\n| `-c` | `--connections` | Number of concurrent connections (default: 10) |\n| `-d` | `--duration` | Test duration in seconds (default: 10) |\n| `-n` | `--num-requests` | Total number of requests (ab-style, overrides `-d`) |\n| `-t` | `--threads` | Number of worker groups (default: 4) |\n| `-m` | `--method` | HTTP method: GET, POST, PUT, DELETE, etc. (default: GET) |\n| `-H` | `--header` | Custom header, e.g. `-H \"Content-Type: application/json\"` (repeatable) |\n| `-b` | `--body` | Request body string |\n| `-p` | `--post-file` | File containing POST body data |\n| `-A` | `--basic-auth` | Basic HTTP auth as `user:pass` |\n| `-C` | `--cookie` | Cookie as `name=value` (repeatable) |\n| `-k` | `--keepalive` | Enable keep-alive (default: on) |\n| | `--no-keepalive` | Disable keep-alive |\n| `-l` | `--verify-length` | Verify response Content-Length consistency |\n| `-v` | `--verbosity` | 0=quiet, 2=warnings, 3=status codes, 4=full detail |\n| | `--timeout` | Request timeout in seconds (default: 30) |\n| `-e` | `--csv` | Write CSV percentile table to file |\n| `-w` | `--html` | Print results as HTML table |\n| | `--json` | Write JSON results to file |\n| `-R` | `--random-param` | Append unique `_cb=\u003cuuid\u003e` query param per request (cache-buster) |\n| | `--rate` | Target requests per second (constant rate mode) |\n| | `--rate-ramp` | Linearly ramp rate from `--rate` to this value over the duration |\n| | `--traffic-profile` | Traffic shaping profile: `sine`, `step`, `sawtooth`, `square`, `spike`, `business-hours`, or `csv:file.csv` |\n| | `--html-report` | Generate interactive Gatling-style HTML report to file |\n| | `--live` | Live TUI dashboard during benchmark (requires `pywrkr[tui]`) |\n| | `--scenario` | Path to JSON/YAML scenario file for scripted multi-step requests |\n| | `--latency-breakdown` | Show detailed per-phase latency breakdown (DNS, TCP, TLS, TTFB, transfer) |\n| | `--threshold` / `--th` | SLO threshold (repeatable), e.g. `--threshold \"p95 \u003c 300ms\"`. Exit code 2 on breach |\n| | `--tag` | Metadata tag as `key=value` (repeatable), e.g. `--tag environment=staging` |\n| | `--otel-endpoint` | Export metrics to OpenTelemetry collector (OTLP/HTTP) |\n| | `--prom-remote-write` | Push metrics to Prometheus Pushgateway endpoint |\n\n### User Simulation Options\n\n| Flag | Long | Description |\n|------|------|-------------|\n| `-u` | `--users` | Number of virtual users (enables simulation mode) |\n| | `--ramp-up` | Seconds to gradually start all users (default: 0) |\n| | `--think-time` | Mean pause between requests per user in seconds (default: 1.0) |\n| | `--think-jitter` | Think time jitter factor 0-1 (default: 0.5, i.e. +/-50%) |\n\n## Output\n\n### Terminal Output\n\n```\n======================================================================\n  BENCHMARK RESULTS\n======================================================================\n  Mode:              300 virtual users, 120.0s\n  Duration:          124.15s\n  Virtual Users:     300\n  Ramp-up:           10.00s\n  Think Time:        1.00s (+/-50%)\n  Avg Reqs/User:     50.8\n  Keep-Alive:        yes\n  Total Requests:    15,229\n  Total Errors:      1\n  Requests/sec:      122.66\n  Transfer/sec:      119.34MB/s\n  Total Transfer:    14.46GB\n\n======================================================================\n  LATENCY STATISTICS\n======================================================================\n    Min:          449.00ms\n    Max:            4.85s\n    Mean:           961.00ms\n    Median:         870.00ms\n    Stdev:          520.00ms\n\n  Latency Percentiles:\n    p50           870.00ms\n    p75             1.10s\n    p90             1.56s\n    p95             2.98s\n    p99             4.85s\n```\n\n### JSON Output\n\nUse `--json results.json` to save structured results:\n\n```json\n{\n  \"duration_sec\": 124.15,\n  \"connections\": 300,\n  \"total_requests\": 15229,\n  \"total_errors\": 1,\n  \"requests_per_sec\": 122.66,\n  \"transfer_per_sec_bytes\": 125120000.0,\n  \"total_bytes\": 15533200000,\n  \"latency\": {\n    \"min\": 0.449,\n    \"max\": 4.85,\n    \"mean\": 0.961,\n    \"median\": 0.87,\n    \"stdev\": 0.52\n  },\n  \"percentiles\": {\n    \"p50\": 0.87,\n    \"p75\": 1.1,\n    \"p90\": 1.56,\n    \"p95\": 2.98,\n    \"p99\": 4.85\n  }\n}\n```\n\n## Benchmarking Modes\n\n### Duration Mode (wrk-style)\n\nRuns for a fixed duration with a pool of persistent connections:\n\n```bash\npywrkr -c 100 -d 30 http://localhost:8080/\n```\n\n### Request-Count Mode (ab-style)\n\nSends exactly N requests, then stops:\n\n```bash\npywrkr -n 10000 -c 50 http://localhost:8080/\n```\n\n### User Simulation Mode\n\nSimulates realistic user behavior with configurable think time and gradual ramp-up:\n\n```bash\npywrkr -u 500 -d 300 --ramp-up 30 --think-time 1.0 http://localhost:8080/\n```\n\nEach virtual user:\n1. Sends a request\n2. Waits for the response\n3. Pauses for think time (with jitter)\n4. Repeats until duration expires\n\nThe ramp-up period gradually introduces users to avoid a thundering herd at startup.\n\n### Cache-Busting Mode\n\nAppend `-R` to any mode to bypass HTTP caches by adding a unique query parameter to each request:\n\n```bash\npywrkr -R -u 300 -d 120 https://example.com/\n# Each request hits: https://example.com/?_cb=\u003cunique-uuid\u003e\n```\n\nThis is useful for testing origin server performance without CDN/proxy cache interference.\n\n### Rate Limiting Mode\n\nInstead of sending requests as fast as possible, `--rate` sends them at a controlled, constant rate. This is critical for SLA testing and finding exact server breaking points.\n\n```bash\n# Constant 500 req/s for 30 seconds\npywrkr --rate 500 -d 30 http://localhost:8080/\n\n# Rate with request count: 50 req/s, stop after 200 requests\npywrkr --rate 50 -n 200 http://localhost:8080/\n\n# Rate limiting with multiple connections (rate is global, shared across all workers)\npywrkr --rate 100 -c 10 -d 60 http://localhost:8080/\n\n# Combine with user simulation (applies when think_time is 0)\npywrkr --rate 200 -u 50 -d 120 --think-time 0 http://localhost:8080/\n```\n\n**Rate Ramp** (`--rate-ramp`): Linearly increase the rate over the test duration. This is useful for finding the exact breaking point automatically:\n\n```bash\n# Start at 100 req/s, linearly increase to 1000 req/s over 60 seconds\npywrkr --rate 100 --rate-ramp 1000 -d 60 http://localhost:8080/\n```\n\nAt `--rate 500`, the tool sends one request every 2ms. If the server cannot keep up (latency exceeds the interval), requests queue up -- this is expected and useful for identifying saturation points.\n\n**Comparison with default \"max throughput\" mode:**\n\n| Mode | Use Case |\n|------|----------|\n| Default (no `--rate`) | Find maximum throughput; stress test |\n| `--rate N` | SLA validation; controlled load; latency-under-load testing |\n| `--rate N --rate-ramp M` | Find breaking point; gradual load increase |\n| `--rate N --traffic-profile P` | Realistic traffic patterns (sine, spikes, CSV replay) |\n\nResults include \"Target RPS\" vs \"Actual RPS\" and \"Rate Limit Waits\" count (how many times the limiter had to slow down a worker).\n\n### Traffic Profiles\n\nShape your test traffic to match real-world patterns using `--traffic-profile`. Requires `--rate` (base/peak rate) and `-d` (duration).\n\n```bash\n# Sine wave: smooth oscillation up to 1000 req/s, 3 cycles\npywrkr --rate 1000 -d 120 --traffic-profile \"sine:cycles=3,min=0.2\" http://localhost:8080/\n\n# Step function: jump between discrete load levels\npywrkr --rate 1000 -d 90 --traffic-profile \"step:levels=100,500,1000\" http://localhost:8080/\n\n# Spike: baseline at 20% with 5x bursts every 10 seconds\npywrkr --rate 200 -d 60 --traffic-profile \"spike:interval=10,multiplier=5\" http://localhost:8080/\n\n# Business hours: 24h daily pattern compressed into test duration\npywrkr --rate 2000 -d 300 --traffic-profile business-hours http://localhost:8080/\n\n# CSV replay: replay real production traffic from a file\npywrkr --rate 1000 -d 300 --traffic-profile \"csv:traffic.csv\" http://localhost:8080/\n```\n\n**Built-in profiles:**\n\n| Profile | Pattern | Use case |\n|---------|---------|----------|\n| `sine` | Smooth wave | Gradual load changes, auto-scaling tests |\n| `step` | Discrete jumps | Testing specific load tiers |\n| `sawtooth` | Repeated ramps | Repeated warm-up behavior |\n| `square` | On/off toggle | Sudden load change recovery |\n| `spike` | Periodic bursts | Flash sale / viral event simulation |\n| `business-hours` | Day/night curve | Realistic daily traffic patterns |\n| `csv:file` | Custom curve | Replaying real production traffic |\n\n**CSV format:** Two columns — `time_sec,rate` (absolute RPS) or `time_sec,multiplier` (factor applied to `--rate`). Values are linearly interpolated between points.\n\n### Latency Breakdown\n\nUse `--latency-breakdown` to see where each request spends its time. This breaks down latency into individual phases using aiohttp's tracing infrastructure:\n\n```bash\n# Show latency breakdown for each phase\npywrkr --latency-breakdown -n 1000 -c 50 https://example.com/\n\n# Combine with JSON output\npywrkr --latency-breakdown --json results.json -d 30 https://example.com/\n```\n\nOutput includes averages with min/max/p50/p95 for each phase:\n\n```\n======================================================================\n  LATENCY BREAKDOWN (averages)\n======================================================================\n    DNS Lookup:          2.15ms  (min=1.20ms, max=5.30ms, p50=2.00ms, p95=4.10ms)\n    TCP Connect:        12.34ms  (min=10.00ms, max=18.50ms, p50=12.00ms, p95=16.20ms)\n    TLS Handshake:      45.67ms  (min=40.00ms, max=55.00ms, p50=45.00ms, p95=52.00ms)\n    TTFB:               89.12ms  (min=60.00ms, max=150.00ms, p50=85.00ms, p95=130.00ms)\n    Transfer:           34.56ms  (min=20.00ms, max=80.00ms, p50=30.00ms, p95=65.00ms)\n    Total:             183.84ms  (min=131.20ms, max=308.80ms, p50=174.00ms, p95=267.30ms)\n\n    New Connections:    50\n    Reused Connections: 950\n```\n\n**Phases:**\n- **DNS Lookup** -- Time to resolve the hostname via DNS\n- **TCP Connect** -- Time to establish the TCP connection\n- **TLS Handshake** -- Time for TLS negotiation (HTTPS only)\n- **TTFB** -- Time to first byte, from sending the request to receiving the first response byte\n- **Transfer** -- Time to read the full response body\n\n**Connection reuse:** When keep-alive is enabled (the default), most requests reuse existing connections. For reused connections, DNS/Connect/TLS phases will be zero. The breakdown reports how many connections were new vs. reused.\n\nWhen `--json` is used, the breakdown data is included in the JSON output under the `latency_breakdown` key.\n\n### Auto-Ramping / Step Load (Autofind)\n\nAutomatically increase load until the server's capacity is found. The `--autofind` flag starts with a small number of users, runs short tests at increasing load levels, and uses binary search to pinpoint the maximum sustainable load.\n\n```bash\n# Find max capacity with default thresholds (1% error rate, 5s p95)\npywrkr --autofind https://example.com/\n\n# Custom thresholds: 0.5% error rate, 2s p95, 15s steps\npywrkr --autofind --max-error-rate 0.5 --max-p95 2.0 \\\n    --step-duration 15 https://example.com/\n\n# Start from 50 users, up to 5000, multiply by 1.5x each step\npywrkr --autofind --start-users 50 --max-users 5000 \\\n    --step-multiplier 1.5 https://example.com/\n\n# Save detailed results to JSON\npywrkr --autofind --json autofind_results.json https://example.com/\n\n# With cache-busting and custom think time\npywrkr --autofind -R --think-time 0.5 https://example.com/\n```\n\n**How it works:**\n\n1. Start with `--start-users` (default: 10) virtual users\n2. Run a short test (`--step-duration`, default: 30s) at that load\n3. Check if error rate exceeds `--max-error-rate` or p95 latency exceeds `--max-p95`\n4. If OK, multiply users by `--step-multiplier` (default: 2x) and repeat\n5. If thresholds exceeded, binary search between the last good and first bad user count\n6. Report the maximum sustainable load with a summary table\n\n**Example output:**\n\n```\n============================================================\n  AUTOFIND RESULTS\n============================================================\n  Maximum sustainable load: 280 users\n\n  Step Results:\n  Users |      RPS |     p50 |     p95 |     p99 | Errors | Status\n     10 |      9.8 |   120ms |   180ms |   200ms |   0.0% | OK\n     20 |     19.5 |   125ms |   190ms |   220ms |   0.0% | OK\n     40 |     38.2 |   130ms |   250ms |   300ms |   0.0% | OK\n     80 |     75.1 |   180ms |   400ms |   600ms |   0.0% | OK\n    160 |    140.2 |   350ms |    1.2s |    2.1s |   0.0% | OK\n    320 |    135.5 |    2.1s |    8.5s |   15.2s |   5.2% | FAIL\n    240 |    138.1 |   800ms |    3.2s |    5.1s |   0.8% | OK\n    280 |    136.8 |    1.1s |    4.8s |    7.2s |   0.9% | OK\n    300 |    135.2 |    1.5s |    5.5s |    9.1s |   1.2% | FAIL\n============================================================\n```\n\n**Autofind options:**\n\n| Flag | Description |\n|------|-------------|\n| `--autofind` | Enable auto-ramping mode |\n| `--max-error-rate` | Stop when error rate exceeds this percent (default: 1.0) |\n| `--max-p95` | Stop when p95 latency exceeds this in seconds (default: 5.0) |\n| `--step-duration` | Duration of each step test in seconds (default: 30) |\n| `--start-users` | Starting number of users (default: 10) |\n| `--max-users` | Maximum users to try (default: 10000) |\n| `--step-multiplier` | Multiply users by this each step (default: 2.0) |\n\n### SLO-Aware Thresholds\n\nDefine pass/fail criteria for your benchmarks. If any threshold is breached, pywrkr exits with code 2 — making it usable in CI/CD pipelines.\n\n```bash\n# Single threshold\npywrkr --threshold \"p95 \u003c 300ms\" -c 100 -d 30 http://localhost:8080/\n\n# Multiple thresholds\npywrkr \\\n    --th \"p95 \u003c 300ms\" \\\n    --th \"p99 \u003c 1s\" \\\n    --th \"error_rate \u003c 1%\" \\\n    --th \"rps \u003e 100\" \\\n    -c 100 -d 30 http://localhost:8080/\n```\n\n**Supported metrics:**\n- `p50`, `p75`, `p90`, `p95`, `p99` — latency percentiles\n- `avg_latency`, `max_latency`, `min_latency` — latency aggregates\n- `error_rate` — error percentage (e.g., `error_rate \u003c 1%` or `error_rate \u003c 1`)\n- `rps` — requests per second\n\n**Operators:** `\u003c`, `\u003e`, `\u003c=`, `\u003e=`\n\n**Time units:** `ms` (milliseconds), `s` (seconds), `us` (microseconds). Default is seconds if no unit.\n\n**Example output:**\n```\n======================================================================\n  SLO THRESHOLDS\n======================================================================\n    p95 \u003c 300ms         Actual: 245.00ms       PASS\n    p99 \u003c 1s            Actual: 820.00ms       PASS\n    error_rate \u003c 1%     Actual: 0.00%          PASS\n    rps \u003e 100           Actual: 523.45         PASS\n\n  Result: ALL THRESHOLDS PASSED\n```\n\n**CI usage:**\n```bash\npywrkr --th \"p95 \u003c 500ms\" --th \"error_rate \u003c 0.1%\" \\\n    -c 50 -d 60 http://api.staging/health || echo \"Performance regression detected!\"\n```\n\n### Observability Export\n\nExport benchmark metrics directly to your observability stack.\n\n#### OpenTelemetry\n\n```bash\npip install pywrkr[otel]\npywrkr --otel-endpoint http://localhost:4318 \\\n    --tag environment=staging --tag build=$(git rev-parse --short HEAD) \\\n    -c 100 -d 30 http://localhost:8080/\n```\n\nExports gauges and counters: `pywrkr.requests.total`, `pywrkr.errors.total`, `pywrkr.requests_per_sec`, `pywrkr.latency.p50/p95/p99/mean/max`, `pywrkr.transfer_bytes_per_sec`, `pywrkr.duration_sec`.\n\n#### Prometheus Remote Write (Pushgateway)\n\n```bash\npywrkr --prom-remote-write http://pushgateway:9091 \\\n    --tag region=us-east-1 --tag service=api \\\n    -c 100 -d 30 http://localhost:8080/\n```\n\nUses stdlib `urllib` — no extra dependencies. Pushes metrics in Prometheus text format to `{endpoint}/metrics/job/pywrkr`.\n\n#### Test Metadata Tags\n\nTags are attached to all exported metrics and included in JSON output:\n\n```bash\npywrkr --tag environment=production --tag build=v2.1.0 \\\n    --tag region=eu-west-1 --tag test_name=api_stress \\\n    --json results.json -c 100 -d 30 http://localhost:8080/\n```\n\n### Multi-URL Mode\n\nTest multiple endpoints in a single benchmark run using a URL file:\n\n```bash\n# Create a URL file (one URL per line)\ncat urls.txt\nhttp://localhost:8080/api/users\nhttp://localhost:8080/api/products\nhttp://localhost:8080/api/orders\n\n# Run benchmark against all URLs\npywrkr --url-file urls.txt -c 50 -d 30\n```\n\n| Flag | Description |\n|------|-------------|\n| `--url-file` | Path to file containing URLs to test (one per line) |\n\nRequests are distributed across all URLs. Results include per-URL breakdowns alongside aggregate statistics.\n\n### Distributed Mode\n\nScale benchmarks across multiple machines by running one master and multiple workers:\n\n```bash\n# On the master node: coordinate 3 workers\npywrkr http://target:8080/ --master --expect-workers 3 -c 300 -d 60\n\n# On each worker node: connect back to the master\npywrkr --worker master-host:9220\n```\n\n| Flag | Description |\n|------|-------------|\n| `--master` | Run as distributed master (coordinates workers) |\n| `--worker HOST:PORT` | Run as distributed worker, connecting to master at HOST:PORT |\n| `--expect-workers` | Number of workers the master should wait for before starting |\n| `--bind` | Master bind address (default: `0.0.0.0`) |\n| `--port` | Master listen port (default: `9220`) |\n\nThe master splits the workload evenly across workers, collects results, and produces a single aggregated report.\n\n## Installation\n\n```bash\n# Basic (aiohttp only)\npip install pywrkr\n\n# With live TUI dashboard\npip install pywrkr[tui]\n\n# With OpenTelemetry export\npip install pywrkr[otel]\n\n# Everything\npip install pywrkr[all]\n```\n\n## Development Setup\n\n```bash\n# Install in editable mode with dev + lint dependencies\npip install -e \".[dev,lint]\"\n```\n\n## Testing\n\n```bash\n# Run all tests\npython -m pytest tests/ -v\n\n# Run a specific test file\npython -m pytest tests/test_pywrkr.py -v\npython -m pytest tests/test_har_import.py -v\n\n# Run a specific test class\npython -m pytest tests/test_pywrkr.py::TestMakeUrl -v\n\n# Run tests sequentially (useful for debugging)\npython -m pytest tests/ -v -n 0\n```\n\nThe test suite includes unit and integration tests covering:\n- Formatting helpers, percentiles, histogram, timeline, CSV/JSON/HTML output\n- Integration tests with a real aiohttp test server (duration mode, request-count mode, POST, auth, cookies, content-length verification, keepalive, cache-buster)\n- User simulation integration tests (think time, ramp-up, jitter, error handling, output formats)\n- Autofind integration tests (healthy server, error endpoint, threshold enforcement, binary search, JSON output, summary table)\n- HAR import tests (parsing, filtering, scenario generation)\n- Reporting module tests (formatting, percentile computation, threshold evaluation, CSV/JSON output)\n- Multi-URL mode tests (URL file loading, entry parsing)\n- Distributed mode tests (config/stats serialization, merge operations, TCP protocol)\n- Worker utility tests (URL construction, headers, stats merging, breakdown aggregation)\n\n## Contributing\n\nContributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for details on how to get started, report bugs, suggest features, and submit pull requests.\n\nThis project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkurok%2Fpywrkr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkurok%2Fpywrkr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkurok%2Fpywrkr/lists"}