从源码构建需要 Crystal >= 1.21.0;Docker 里访问宿主机 localhost 服务需加 --network host;--workers 并非越大越好,超过 8 后实测吞吐反而下降。

cryload
sdogruyolMIT活跃维护用 Crystal 写的单二进制 HTTP 压测工具,面向 CI/CD 流水线,提供真实延迟统计、阈值校验和 JSON 输出,方便在流水线里卡性能回归。
HTTP load testing for CI/CD — honest latency, CI thresholds, single binary, JSON output
curl -sSfL https://raw.githubusercontent.com/sdogruyol/cryload/master/scripts/install.sh | sh -s
cryload https://example.com -n 1000 -c 50README 未说明 GPU 或显存要求;这是纯 CPU 的 HTTP 压测工具,Linux、macOS、Windows 均可运行,官方提供 amd64/arm64 的 Docker 镜像。
面向 CI/CD 的单二进制 HTTP 压测工具,用修正延迟和阈值门禁判断部署是否拖慢性能
用 Crystal 写的单二进制 HTTP 压测工具,面向 CI/CD 流水线,提供真实延迟统计、阈值校验和 JSON 输出,方便在流水线里卡性能回归。
核心能力
- 用协调遗漏修正的 corrected_latency 计算 p99 等分位
- 提供 --max-p50 到 --max-p999 等阈值矩阵,超限退出码 1
- 按 DNS、连接、TLS、TTFB 拆分阶段耗时定位瓶颈
- 支持 JSON/CSV/quiet 输出,schema 带版本号便于流水线解析
- 多核 --workers 并发压测,单文件约 1.4 MB 无运行时依赖
容易踩的坑
- v6.1.0最新正式版2026-10-11 · 2026-10-11 14:56 UTC
- Documentation — Public descriptions match the README: honest latency and CI thresholds, no stale throughput number.
shard.yml, the Docker image,--help, and the installers now share that positioning. - Reporting — A refused connection is reported as
connect_refusedagain when cryload is built with Crystal 1.21.1+. Crystal 1.21.1 (crystal-lang/crystal#17436) swapped the real connect error for a stale errno (connect: Operation now in progress), so refusals showed up asconnect_failed. cryload patches the Unix event loops'connectuntil upstream ships the fix - Reporting — A connect that exceeds
--timeoutis reported asconnect_timeout. The stdlib raises a bareIO::TimeoutErrorfor it, so it used to land inread_timeoutandconnect_timeoutwas never emitted - CLI — Numeric flags reject
nan,infand out-of-range values with a config error (exit 2).--rate inf,-d infor--rate 1e-10used to crash withOverflowError, and--max-p99 nancrashed withJSON::Errorafter the whole run, all exiting 1 — the threshold-breach code.--ratenow needs at least1e-9requests/sec, and huge finite limits (--max-p99 1e300) render instead of overflowing - Proxy — HTTPS targets through
--proxynow honour--timeouton the tunnel (CONNECT reply, TLS handshake and every request; a stalled proxy used to block the worker forever), reconnect after the target closes a keep-alive connection instead of failing every later request asother, close the socket when CONNECT or the handshake fails, and supporthttps://proxies and IPv6 proxy literals. Tunnel phases:dns/connectdescribe the proxy hop (connectincludes the CONNECT exchange),tlsthe handshake with the target - Proxy — Plain-HTTP targets through
--proxy http://user:pass@...now sendProxy-Authorization; only the CONNECT tunnel used to carry the credentials - Accuracy — Transfer metrics count response bodies as received. cryload still sends
Accept-Encoding: gzip, deflateby default, but no longer inflates the body, sotransfer.*and "Total data" report compressed bytes for a compressing target, and the generator spends no CPU on zlib. An explicit-H Accept-Encodingstill wins - Accuracy — The process exits with the exact code the report printed. The gates used to be evaluated again after the report was written, and in request-count mode the measurement window stayed open meanwhile, so a slow stdout reader could turn a printed
verdict.exit_code: 0into exit 1 under--min-rps. The window now closes when the last request is merged - Accuracy — Request-count workers flush their stats at least once a second, like duration workers. A worker whose
-nshare was below 250 requests stayed invisible until it finished, so progress and a Ctrl-C report of a slow run showed 0 requests - Toolchain — Release binaries, the Docker image and CI are built with Crystal 1.21.1 (was 1.21.0). It carries crystal-lang/crystal#17436, which fixes non-blocking connect on macOS 26.7: a refused connection was treated as established, so a
localhostthat resolves to::1first never fell back to127.0.0.1. The minimum Crystal for building from source stays 1.21.0
- Documentation — Public descriptions match the README: honest latency and CI thresholds, no stale throughput number.
- v6.0.02026-08-29 · 2026-08-29 18:21 UTC
查看更新说明
- Breaking (Concurrency) — Multi-core load generation is now the default. v5 ran every load fiber on a single thread; v6 sizes the execution context to
min(cpu_count, connections)OS threads and exposes it as--workers N.--workers 1reproduces the v5 single-core path; the cpu default respectsCRYSTAL_WORKERS - Breaking (Accuracy) — Threshold gates now evaluate corrected latency by default (
--latency-correction on). A run that passed--max-p99in v5 can fail in v6 when the target could not keep up with--rate; corrected and uncorrected latency are identical when--rateis not set. Pass--latency-correction offfor v5 gate semantics - Breaking (Exit codes) — Exit codes are now a taxonomy instead of a single failure code: usage/config errors moved from
1to2, an unreachable target (zero HTTP responses plus at least one transport error) moved from1to3, and signals now exit130(SIGINT) /143(SIGTERM) instead of0.1is reserved for breached thresholds, and a breached threshold takes precedence over a signal code
Code Meaning 0 Run finished, no threshold breached 1 A threshold was breached 2 Usage/config error 3 Target unreachable 130 Interrupted (SIGINT) 143 Terminated (SIGTERM) - Breaking (Reporting) — Transport error categories are a fixed, normalized vocabulary instead of Crystal exception class names:
dns_failure,connect_refused,connect_timeout,connect_failed,read_timeout,write_timeout,request_timeout,connection_reset,connection_closed,tls_error,proxy_error,protocol_error,other. Consumers matching onIO::TimeoutErrororSocket::ConnectErrormust switch to the new names - Breaking (JSON) — JSON output gained a top-level
schema_version, currently1. Every documented key is always present and isnullwhen not applicable, so consumers can stop probing for key existence - Breaking (CLI) —
--progressnow defaults to on only when stderr is a TTY, so CI logs are clean without passing--no-progress. Explicit--progressstill forces it on - Breaking (Toolchain) — Minimum Crystal is now 1.21.0 (was 1.19.0); multi-core load generation uses
Fiber::ExecutionContext.default.resize - Performance —
--workers 8 -c 128reaches a median 329,440 req/s against aSO_REUSEPORTtarget on loopback, versus 109,161 req/s on the single-thread--workers 1path — a 3.0x gain (AMD Ryzen AI 9 465, 20 threads, 2-byte body, median of 5 three-second runs).--workers 16measures slower (280,079 req/s) than--workers 8on the same box, which is why the default ismin(cpu cores, connections)rather than every core - Performance — Response bodies are streamed through one fixed buffer per worker instead of being materialised as a
String, so peak RSS no longer scales with response size: 22.9 MB at--workers 8 -c 128with 2-byte bodies, 12.9 MB at--workers 8 -c 32with 100 KB bodies - Accuracy — Added coordinated-omission correction. v5 measured latency only from the moment a request was actually sent, so a stalled target made the numbers look better: in a regression test that froze the target for 1 second during
-q 500 -d 5, v5 reported p99 = 0.37 ms while silently dropping 490 of 2500 scheduled requests. v6 reports the outage incorrected_latency_ms.p99and surfaces the loss asrate.skipped_requests - Accuracy — Added a
rateblock reporting requested vs attained req/s, attainment percent, scheduled and skipped requests, and schedule drift, so a rate-limited run can no longer claim a rate it never achieved - Accuracy — Added
send_delay_mspercentiles, the gap between a request's scheduled send time and its actual send time - Accuracy —
--timeoutnow sets the write timeout as well as connect and read; v5 left writes unbounded - Accuracy —
-q/--rateaccepts fractional rates (-q 0.5) - CI/CD — Adde
- Breaking (Concurrency) — Multi-core load generation is now the default. v5 ran every load fiber on a single thread; v6 sizes the execution context to
- v5.2.12026-08-11 · 2026-08-11 08:19 UTC
查看更新说明
- GitHub Action — Shortened the
action.ymldescription to 117 characters; GitHub Marketplace rejects listings whose description is 125 characters or longer, which blocked publishing v5.2.0
- GitHub Action — Shortened the
- v5.2.02026-08-11 · 2026-08-11 08:06 UTC
查看更新说明
- Distribution — Added a Docker image at
ghcr.io/sdogruyol/cryload: multi-arch (amd64/arm64), ~9 MB Alpine runtime around the same fully static binary as the Linux release
docker run --rm ghcr.io/sdogruyol/cryload https://example.com -n 1000 -c 50- Distribution — Added a composite GitHub Action so cryload runs in a workflow step with thresholds and JSON results as step outputs; every CLI flag is exposed as an input, and
uses: sdogruyol/cryload@v5tracks the latest v5 release
- uses: sdogruyol/cryload@v5 with: url: http://127.0.0.1:3000/api requests: 500 max_p99: "250"- Releases — Windows release binaries are now linked statically; earlier builds dynamically linked the Crystal toolchain's DLLs and failed to start on machines without Crystal installed
- Releases — Release smoke tests now cover Windows, so a broken Windows binary fails the release instead of shipping
- Installer —
scripts/install.shusesGITHUB_TOKENfor the latest-release API lookup when set, avoiding the anonymous per-IP rate limit shared across hosted CI runners - Installer —
GITHUB_URLnow also redirects the release API lookup, so enterprise mirrors query their own/api/v3endpoint and a mirror token is never sent to public GitHub - Documentation — Added docs/github-action.md with the full input/output reference and version-pinning rules
- Distribution — Added a Docker image at
- v5.1.02026-08-01 · 2026-08-01 13:18 UTC
查看更新说明
- Releases — Linux release binaries are now fully static (musl/Alpine), so they run on any distro without matching glibc/OpenSSL versions
- Releases — macOS release binary now links OpenSSL statically; release smoke tests cover HTTPS
- Resilience — Static OpenSSL builds ignore the host
openssl.cnfby default (OPENSSL_CONF→ null device) so foreign crypto-policy configs cannot abort TLS - Resilience — On macOS, TLS falls back to the system CA bundle (
/etc/ssl/cert.pem) when the OpenSSL default cert path is missing - Documentation — README rewritten for CI/CD positioning; added FAQ and moved the full comparison table to docs/comparison.md
- Packaging — Updated
shard.ymldescription for shard registries
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
cryload - HTTP load testing for CI/CD
Cross-platform, single-binary HTTP load testing CLI. Honest latency, CI thresholds, JSON/CSV output. A modern alternative to ab / wrk / hey, written in Crystal.
Quick start
curl -sSfL https://raw.githubusercontent.com/sdogruyol/cryload/master/scripts/install.sh | sh
cryload https://example.com -n 1000 -c 50
1000 requests, 50 concurrent connections, JSON/CSV/plain output. That's it.
Why cryload?
Existing tools work fine on your laptop. cryload is built for the one place that matters most: CI/CD pipelines.
| Problem | Solution |
|---|---|
| "Did my deploy break performance?" | Set latency thresholds that fail your build |
| "Where do I put the results?" | JSON and CSV output, ready to parse |
| "Different OS in CI vs local?" | Single binary for Linux, macOS, Windows |
| "Which tool works in all three?" | cryload does |
If you need a graph on your laptop, use wrk. If you need to fail a pipeline when p99 goes over 200ms, use cryload.
Features
- ⚡ Multi-core load generation (
--workers), concurrent connections (-c) - ⏱️ Duration or request count mode
- 📊 Latency percentiles: p50, p75, p90, p95, p99, p999 + histogram
- 🎯 Honest latency: coordinated-omission-corrected percentiles and rate attainment
- 🔍 Phase breakdown: DNS, connect, TLS, TTFB — see where the time actually went
- 🗂️ Per-URL and per-status breakdowns, not just one global number
- 🎯 CI threshold matrix:
--max-p50…--max-p999,--max-avg,--max-latency,--min-rps,--max-fail-rate, per-endpoint--url-threshold - 🚦 Exit code taxonomy: slow service, unreachable target and bad config are different codes
- 📦 JSON / CSV / quiet output for pipelines, with a versioned schema
- 🔒 Rate limiting, warmup, keep-alive control, request deadlines, TLS skip
- 🌐 Multi-URL, redirects, proxy, cookies, custom success codes
- 🖥️ Cross-platform: Linux, macOS, Windows - single binary
Performance
cryload is fast. Written in Crystal, compiled to native code, and multi-core by default since 6.0.0.
All figures below are the median of 5 runs, with the observed spread, because a single run of a load generator on a shared machine is noise, not a number.
| Test | Throughput (median of 5) | Spread | p99 |
|---|---|---|---|
--workers 8 -c 128 -d 3 | 329,440 req/sec | 296k – 342k | 1.95 ms |
--workers 16 -c 128 -d 3 | 280,079 req/sec | 278k – 301k | 2.34 ms |
--workers 4 -c 128 -d 3 | 210,668 req/sec | 190k – 240k | 2.38 ms |
--workers 1 -c 128 -d 3 (v5-equivalent single-thread path) | 109,161 req/sec | 108k – 114k | 2.94 ms |
--workers 1 -c 64 -d 3 | 138,106 req/sec | 110k – 143k | 1.32 ms |
Peak RSS (--workers 8 -c 128, 2-byte bodies) | 22.9 MB |
Peak RSS (--workers 1 -c 64, 2-byte bodies) | 15.7 MB |
Peak RSS (--workers 8 -c 32, 100 KB bodies) | 12.9 MB |
| Binary size | ~1.4 MB stripped, single file, no runtime dependencies |
Large responses cost less memory than many small ones, because bodies are streamed through one fixed buffer per worker rather than materialised as a String.
Multi-core buys 3.0x on this hardware. Note that --workers 16 measures
slower than --workers 8: once the schedulers outnumber the useful work they
fight each other and the target for cores. That is why the default is
min(cpu cores, connections) rather than "all of them", and why --workers is a
dial worth turning rather than a number to max out.
Methodology. The target is a local Crystal HTTP::Server returning a 2-byte
body, running as 7 processes sharing the port via SO_REUSEPORT so that the
target is not the bottleneck. Hardware is an AMD Ryzen AI 9 465 (20 threads),
over loopback, release build, 3-second runs, median of 5.
These are loopback numbers on one machine, not a claim about your network or your application. They exist to show the load generator's own ceiling — the number you need before you can trust that a measurement is about your server. Reproduce them with the commands in the table:
cryload http://localhost:3000 --workers 8 -c 128 -d 3
cryload http://localhost:3000 --workers 1 -c 128 -d 3
If throughput rises when you raise --workers, your earlier numbers were
measuring the load generator, not the target. And look at that spread column
before you set a --min-rps gate from a single local run.
No JVM, no Node. Just a single binary that starts instantly and uses almost no memory.
Honest latency
Most load testers measure latency from the moment a request is sent. That sounds reasonable until the target stalls. While it is stalled, no requests get sent — so no slow measurements get recorded — and the tool reports the handful of fast responses from before and after the freeze. A target that stops responding makes the numbers look better. This is called coordinated omission.
It is not a rounding error. Here is cryload's own regression test: freeze the target for 1 second during a -q 500 -d 5 run.
| v5 | v6 | |
|---|---|---|
| Service p99 (measured from the send) | 0.37 ms | 0.56 ms |
| p99 the gate actually reads | 0.37 ms | 951.96 ms (corrected_latency_ms.p99) |
| Scheduled requests | 2500 | 2500 |
| Requests actually issued | 2010 | 2500 |
| Dropped without a word | 490 | 0 (rate.skipped_requests) |
| Peak send delay reported | — | 981 ms (rate.schedule_drift_ms) |
--max-p99 200 verdict | PASS, exit 0 | FAIL, exit 1 |
Read the second row twice. Same run, same server, same one-second outage: v5 reported a p99 of 0.37 ms and passed the gate, while quietly discarding 490 of the 2500 requests it was supposed to send. v6 reports 951.96 ms and fails the build. Note that v6's service p99 is still sub-millisecond and that is correct — the requests really were served fast once they got out. The outage lives in the waiting, which is exactly what a user would have felt.
v6 fixes this in three places:
corrected_latency_mscharges each request for the time it spent waiting for its scheduled send slot, plus its service time. A request that was supposed to go out at t=1.0s but went out at t=2.0s is a 1-second-plus request, because that is what a real user would have experienced.rate.skipped_requestsandrate.attainment_percentreport the schedule slots that were never issued, so a run can no longer claim a rate it did not achieve.- Threshold gates evaluate corrected latency by default, so
--max-p99 250fails on that frozen target instead of passing. Add--fail-on-rate-missto also fail when the attained rate falls more than 1% short.
Corrected and uncorrected latency are identical when --rate is not set: without a request schedule there is no send delay to correct for. Pass --latency-correction off for v5 gate semantics.
Installation
Option 1: Install script (recommended)
Linux / macOS:
curl -sSfL https://raw.githubusercontent.com/sdogruyol/cryload/master/scripts/install.sh | sh -s
Windows (PowerShell):
iwr -useb https://raw.githubusercontent.com/sdogruyol/cryload/master/scripts/install.ps1 | iex
Option 2: Prebuilt binary
Download from Releases:
chmod +x cryload-linux
./cryload-linux --help
Option 3: Docker
docker run --rm ghcr.io/sdogruyol/cryload https://example.com -n 1000 -c 50
Multi-arch (amd64/arm64) image, ~9 MB. Use --network host on Linux to reach services on the host's localhost. Tags follow the release version without the v prefix: 6.0.0, 6.0, 6, latest.
Option 4: GitHub Action
- uses: sdogruyol/cryload@v6
with:
url: http://127.0.0.1:3000/api
requests: 500
max_p99: "250"
See docs/github-action.md for all inputs and outputs.
Option 5: Build from source
Requires Crystal >= 1.21.0.
git clone https://github.com/sdogruyol/cryload.git && cd cryload
shards build --release
Usage
cryload <url> [options]
| Option | Description |
|---|---|
-n, --numbers | Number of requests |
-d, --duration | Test duration (30, 30s, 2m, 1h30m, 500ms) |
-c, --connections | Concurrent connections (default: 10) |
--workers | OS threads running the load (default: min(cpus, connections); --workers 1 reproduces v5 behaviour) |
-m, --method | HTTP method (default: GET) |
-b, --body | Request body |
--body-file | Read body from file |
--body-stdin | Read body from stdin |
-H, --header | Repeatable header (-H "Key: Value") |
-a, --basic-auth | Basic auth (user:password) |
--user-agent | User-Agent override |
--host-header | Explicit Host header |
--timeout | Connect/read/write timeout, with units |
--request-timeout | Total deadline for one request, including body download |
-q, --rate | Rate limit in req/sec, fractional allowed (-q 0.5) |
--latency-correction | on (default) or off — gate on corrected latency |
-L, --follow-redirects | Follow redirects |
--disable-keepalive | New connection per request; setup cost counts as latency |
--output-format | text, json, csv, quiet |
--json | Shorthand for --output-format json |
--success-status | Custom success codes/ranges |
--insecure | Skip TLS verification |
--warmup | Untimed warmup before the benchmark, with units |
--proxy | HTTP(S) proxy |
--cookie | Repeatable cookie (name=value) |
--urls-file | Load target URLs from file |
--random-path | Append random path per request |
--progress / --no-progress | Live stderr progress (default: on only when stderr is a TTY) |
-V, --version | Print the installed release |
Threshold flags are in CI/CD below.
Common examples
# 10K requests, 100 concurrent
cryload http://localhost:3000 -n 10000 -c 100
# 30 seconds, 50 connections
cryload http://localhost:3000 -d 30s -c 50
# POST with JSON body
cryload http://localhost:3000/api -n 500 -m POST \
-H "Content-Type: application/json" \
-b '{"name":"cry"}' --timeout 5
See docs/examples.md for more.
CI/CD
cryload is built for pipelines. Use --json or --output-format csv for structured output, --output-format quiet for exit-code-only checks.
Threshold matrix
Any breached threshold exits 1.
| Flag | Effect |
|---|---|
--max-p50 MS | Exit 1 if p50 exceeds MS |
--max-p75 MS | Exit 1 if p75 exceeds MS |
--max-p90 MS | Exit 1 if p90 exceeds MS |
--max-p95 MS | Exit 1 if p95 exceeds MS |
--max-p99 MS | Exit 1 if p99 exceeds MS |
--max-p999 MS | Exit 1 if p999 exceeds MS |
--max-avg MS | Exit 1 if average latency exceeds MS |
--max-latency MS | Exit 1 if the maximum observed latency exceeds MS |
--min-rps N | Exit 1 if throughput is below N req/s |
--max-fail-rate PCT | Exit 1 if the failure rate exceeds PCT |
--fail-on-error | Exit 1 on any HTTP or transport error |
--fail-on-transport-error | Exit 1 on any transport error |
--fail-on-rate-miss | Exit 1 if the attained rate is more than 1% below --rate |
Latency gates evaluate corrected latency by default; --latency-correction off restores v5 semantics. See Honest latency.
Per-endpoint gates
--url-threshold "PATTERN METRIC VALUE" is repeatable and scopes a gate to the target URLs whose text contains PATTERN:
cryload --urls-file urls.txt -d 30s -c 50 \
--url-threshold "/api/users max-p99 120" \
--url-threshold "/api/search max-p99 400" \
--url-threshold "/api/orders max-fail-rate 0.5" \
--min-rps 2000
METRIC is one of max-p50 max-p75 max-p90 max-p95 max-p99 max-p999 max-avg max-latency max-fail-rate min-rps. A pattern that matches no target URL is a config error (exit 2), so a renamed endpoint fails the build instead of silently un-gating itself.
Exit codes
| Code | Meaning |
|---|---|
0 | Run finished, no threshold breached |
1 | A threshold was breached |
2 | Usage/config error (bad flag, bad URL, missing file, unresolvable --url-threshold pattern, fd limit too low) |
3 | Target unreachable: zero HTTP responses and at least one transport error |
130 | Interrupted (SIGINT) |
143 | Terminated (SIGTERM) |
A breached threshold takes precedence over a signal code. The same information is in the JSON report as verdict.exit_code and verdict.reason.
GitHub Actions example
Use the official action — it installs cryload, runs the benchmark, and exposes the JSON results as step outputs:
- name: Latency SLA
id: bench
uses: sdogruyol/cryload@v6
with:
url: http://localhost:3000/api
requests: 500
max_p99: "250"
- name: Show p99
if: always()
run: echo "p99 was ${{ steps.bench.outputs.p99 }} ms"
See docs/github-action.md for all inputs and outputs. Or install the binary directly:
- name: Install cryload
run: curl -sSfL https://raw.githubusercontent.com/sdogruyol/cryload/master/scripts/install.sh | sh -s
- name: Latency SLA
run: |
cryload http://localhost:3000/api -d 30s -q 500 \
--max-p99 250 --fail-on-rate-miss --json > result.json
jq -e '.schema_version == 1' result.json
jq -r '.thresholds.breached[] | "FAIL \(.name) \(.metric)=\(.actual) limit=\(.limit)"' result.json
--no-progress is no longer needed: progress is only enabled when stderr is a TTY, so CI logs stay clean by default.
How cryload compares
| Feature | cryload | ab | hey | oha | wrk |
|---|---|---|---|---|---|
| CI/CD output (JSON/CSV/quiet) | ✅ | - | JSON | JSON | - |
| CI threshold exit codes | ✅ | - | - | - | - |
| Per-endpoint threshold gates | ✅ | - | - | - | - |
| Exit code taxonomy | ✅ | - | - | - | - |
| Coordinated-omission correction | ✅ | - | - | - | - |
| Latency phase breakdown | ✅ | - | partial | partial | - |
Rate limiting (--rate) | ✅ | - | partial | ✅ | - |
| Multi-core load generation | ✅ | - | ✅ | ✅ | ✅ |
| HTTP/2 | - | - | ✅ | ✅ | - |
| Scriptable load (Lua) | - | - | - | - | ✅ |
| Cross-platform binary | ✅ | Linux | ✅ | ✅ | Linux |
Full matrix: docs/comparison.md.
Built with Crystal
cryload is written in Crystal. Ruby-like syntax, compiled speed, single-binary deployment.
FAQ
Why not just use ab / hey / wrk?
Those tools are great for local benchmarks, and wrk still wins on Lua-scripted scenarios. cryload is built for CI/CD: coordinated-omission-corrected latency, a threshold matrix you can scope per endpoint, an exit code taxonomy, JSON/CSV output with a versioned schema, and cross-platform binaries. Since 6.0.0 it also saturates every core by default, so multi-core is no longer a reason to reach for another tool. If you want to fail a pipeline when p99 goes over 200ms — and be sure that p99 is honest — use cryload.
Can I use cryload for DDoS?
No. cryload is designed for testing your own servers and CI pipelines. Do not use it against targets you don't own.
Does cryload support HTTP/2?
Not yet. HTTP/1.1 only for now. HTTP/2 is on the roadmap.
What is coordinated omission and why should I care?
Most load testers start the latency clock when a request is sent. When the target stalls, nothing gets sent, so nothing slow gets measured — and the report shows only the fast responses from either side of the stall. A target that stops responding therefore looks faster. In cryload's own regression test, freezing the target for one second during a -q 500 -d 5 run made v5 report p99 = 0.37 ms while it silently dropped 490 of 2500 scheduled requests. v6 charges each request for the time it waited for its scheduled slot (corrected_latency_ms), reports the slots it never issued (rate.skipped_requests), and gates on corrected latency by default. See Honest latency.
Why did my exit code change from 1 to 2/3?
v5 returned 1 for every failure. v6 splits them so a pipeline can tell them apart: 1 is a breached threshold (a real regression to triage), 2 is a usage or config error (a broken command line or workflow file), and 3 is an unreachable target (nothing answered — usually a broken test harness, not a slow service). Signals now exit 130/143 instead of 0, so a killed run no longer looks like a pass. If your script did if cryload ...; then, it still behaves the same; if it matched on == 1, widen it to != 0 or switch on the code — docs/examples.md has a template.
Is there a Docker image?
Yes: docker pull ghcr.io/sdogruyol/cryload. The single binary is still the lightest option, but the image is handy for containerized pipelines (GitLab CI, Kubernetes jobs, etc.).
Why is it written in Crystal?
Crystal compiles to a single native binary with no runtime. It starts instantly, uses minimal memory, and delivers C-like performance with Ruby-like syntax.
Sponsors
If cryload helps your CI pipeline, consider sponsoring. Every dollar helps me keep building open source tools full time.
License
MIT