A lightweight, arrival-rate HTTP load generator for .NET. Uses an open-loop producer-consumer model built around a bounded Channel<T> — no external dependencies beyond the .NET runtime.
The tool uses a three-component design:
- Producer — schedules request tickets at the target arrival rate, independent of how long requests take.
- Bounded channel — absorbs short bursts without allowing unbounded queue growth; excess tickets are dropped and counted.
- Consumer pool — a fixed number of workers that greedily pull tickets and fire HTTP requests in parallel.
Because the producer is decoupled from response latency, the configured request rate is open-loop: if the service slows down, the producer still attempts to schedule work at the target rate. When consumers can't keep up, the backlog grows and the tool starts dropping scheduled requests once the queue fills. The report surfaces this through dropped, queue-delay, and status-code distributions so you can see exactly where the system saturates.
dotnet build Hammer.slnxdotnet run --project src/Hammer.Console -- \
--url "https://example.com/api/endpoint" \
--method GET \
--target-rps 150 \
--duration 00:02:00 \
--consumers 64 \
--max-backlog 500 \
--timeout 00:00:15POST with a bearer token and request body:
dotnet run --project src/Hammer.Console -- \
--url "https://example.com/api/endpoint" \
--method POST \
--body "{}" \
--content-type "application/json" \
--bearer-token "<jwt>" \
--target-rps 75 \
--duration 300 \
--consumers 48 \
--max-backlog 300| Option | Default | Description |
|---|---|---|
--url |
(required) | Target URL |
--method |
GET |
HTTP method |
--target-rps |
(required) | Target requests per second |
--duration |
00:01:00 |
Test duration (hh:mm:ss or seconds) |
--consumers |
32 |
Number of concurrent worker tasks |
--max-backlog |
256 |
Bounded channel capacity; excess tickets are dropped |
--timeout |
00:00:30 |
Per-request timeout (hh:mm:ss or seconds) |
--body |
Request body | |
--content-type |
application/json |
Content-Type header (used with --body) |
--bearer-token |
Authorization: Bearer token | |
--header |
Extra headers in Name: Value format; repeatable |
Scheduled: 9000 Enqueued: 8874 Dropped: 126
Started: 8874 Completed: 8874
Successes: 8701 Failures: 173 Success rate: 98.1%
Target RPS: 150.0 Start RPS: 147.9 Completion RPS: 139.4
Latency (ms)
avg p50 p95 p99 min max
84.2 71.0 210.5 388.1 12.3 921.4
Queue delay (ms)
avg p50 p95 p99 min max
2.1 0.8 9.4 28.0 0.0 143.2
Status codes
200: 8701 timeout: 112 503: 61
- Dropped — tickets discarded because the channel was full. Rising drops mean consumers can't keep up.
- Queue delay — how long a ticket waited before a consumer picked it up. Growing queue delay is an early signal of overload.
- Latency — wall-clock time from when a consumer started a request to when it completed.
- Keep
--max-backlogbounded. An unbounded queue hides the overload signal. - Watch
droppedand queue-delay p95/p99 as saturation indicators — they react before success rate degrades. - Tune
--consumersto match the concurrency your service can handle; too few workers starve the target, too many can skew latency by exhausting server thread pools.