Performance testing HTTP APIs with wrk and wrk2

HTTP APIs can look healthy in functional tests while slowing down under concurrent traffic. A few successful requests from Postman or a Java integration test say little about queueing, connection reuse, tail latency, or what happens when demand rises faster than the service can respond. Lightweight command-line tools such as wrk and wrk2 make those behaviours visible without requiring a large test platform.

The two tools share a familiar interface, but they answer different performance questions. wrk is useful for generating as much traffic as the system can handle and observing throughput under pressure. wrk2 adds a constant-rate workload, making it better for controlled latency measurement and for avoiding coordinated omission. Used carefully, both tools fit well into a practical quality engineering workflow.

What wrk measures well

wrk is a high-performance HTTP benchmarking tool designed to create substantial concurrent load with relatively little client overhead. Its main options control threads, connections, test duration and the target URL. A simple baseline might look like this:

wrk -t4 -c100 -d60s https://api.example.com/health

This starts four worker threads, maintains 100 connections and runs for 60 seconds. The output includes requests per second, transfer rate and latency statistics. The result is useful for discovering the point at which CPU, memory, database connections, or another dependency becomes the limiting factor.

The distinction between connections and threads matters. A thread can manage many sockets, while -c controls the number of open client connections. Increasing both values blindly can overload the load generator before it meaningfully exercises the API. Run the client from a suitable machine, check its CPU and network usage, and confirm that TLS negotiation or local bandwidth is not the bottleneck.

wrk can also execute Lua scripts for dynamic requests. Scripts can add headers, construct POST bodies, select paths and process responses. This makes it practical for authenticated endpoints, JSON payloads and several request types in one test, though complex business workflows may be easier to express in a dedicated framework.

Where wrk2 changes the test

wrk generally tries to send requests as quickly as possible. Its achieved request rate therefore depends on server response time. If the service becomes slower, fewer new requests are started, and the test may underrepresent the latency users would experience when traffic continues arriving at the original rate.

wrk2 introduces a target request rate, commonly supplied with -R. For example:

wrk -t4 -c100 -d60s -R2000 https://api.example.com/orders

The exact binary must be a wrk2 build or a compatible fork that supports this option. The test attempts to begin 2,000 requests per second, subject to available connections and client capacity. This open-loop style is valuable when checking an API’s response-time distribution at a known arrival rate.

Its main advantage is more trustworthy tail latency under saturation. If requests take longer to finish, they still continue to arrive at the planned rate until the client or server reaches its limit. That can expose queue growth, rising p95 and p99 values, and error behaviour that a closed-loop benchmark may conceal. A constant-rate test is especially helpful for capacity planning and service-level objectives.

Preparing realistic HTTP traffic

Start with a safe endpoint and a small workload. Verify the URL, HTTP method, authentication, headers and response status before increasing concurrency. For a JSON request, a Lua script can set Content-Type, add an authorisation header and return a body generated for each request. Avoid placing real customer tokens or personal information in source-controlled scripts.

The workload should represent how the API is actually used. A read-only health endpoint may be cheap because it bypasses a database, while a search endpoint can involve indexes, serialisation and downstream calls. Include realistic payload sizes, cache states and response codes. If a workflow needs a token followed by several dependent calls, model that dependency rather than pretending every request is independent.

Test data requires particular care in Australia. A staging environment may contain records linked to identifiable customers, and the Privacy Act 1988 can make careless handling of personal information a serious governance issue. Use synthetic users, isolated accounts and short-lived credentials. For an Australian retail API, it can be useful to model a Melbourne or Sydney customer’s postcode and delivery options without copying real addresses.

The script itself should remain simple enough to inspect. Add a unique request identifier where the application supports it, and log a small sample of failures rather than every successful response. Excessive client-side logging can distort the benchmark and fill disk space before the server shows meaningful pressure.

Designing a useful load profile

A single 60-second spike rarely represents production behaviour. A better profile has a warm-up period, a steady measurement window and several controlled changes in load. For example, begin at 500 requests per second, hold that rate for two minutes, increase to 1,000, then repeat until the error budget or latency target is breached.

A ramp-up reveals capacity thresholds, while a longer steady state exposes memory leaks, connection pool exhaustion and garbage-collection effects. Include a short cool-down where possible. Compare results from cold and warm caches, and repeat important runs because a single benchmark can be affected by noisy neighbours, autoscaling timing or database maintenance.

Australian services often have distinct traffic patterns. A Brisbane delivery platform may see evening demand after work, while a national retailer can experience sharp bursts during a Sydney or Melbourne promotion. NBN and mobile network conditions also affect end-user latency, but they should not be confused with server processing time. Keep the load generator close to the deployed service, such as an appropriate Australian cloud region, when measuring backend capacity.

Use a modest number of virtual users for a first pass. Increase -c only when the target rate cannot be achieved with the current connection count. If wrk2 reports that it cannot sustain the requested rate, the limitation may be the client, the network, or too few connections rather than the API itself.

Reading latency beyond the average

Average latency is easy to understand and easy to misuse. Suppose most requests complete in 40 milliseconds but a small group waits several seconds for a database lock. The average might still look acceptable while real users experience intermittent failures. Review median, p90, p95, p99 and maximum latency alongside throughput and error rate.

Tail latency needs context. A p99 of 800 milliseconds may be acceptable for a batch export but poor for an autocomplete API. Define targets per endpoint and connect them to observable user journeys. Record server-side timings, database wait events, garbage collection, CPU, memory, network saturation and downstream response times during the same run.

Status codes are equally important. A high requests-per-second figure achieved through 429, 500 or 503 responses is not capacity. Track successful responses separately from all completed requests, and inspect malformed responses or unexpectedly small payloads. A Lua response hook can count failures, while application metrics should provide the authoritative view.

When comparing wrk with wrk2, explain the workload clearly. A wrk result at maximum throughput and a wrk2 result at a fixed 2,000 requests per second are not interchangeable. Store the command, script revision, application version, environment, rate, connection count and test time with each result so later comparisons remain meaningful.

Integrating benchmarks into quality engineering

Performance checks work best when they complement functional and unit tests. Unit tests still protect parsing, validation and business rules; for example, a test suite covering Java stream and lambda behaviour can be strengthened using stream unit testing before performance work begins. A benchmark should then verify how the assembled service behaves under concurrent access.

Keep a small smoke benchmark in continuous delivery. It might run for 30 seconds against a disposable environment and fail when latency or error thresholds regress significantly. Run larger capacity tests on demand or on a scheduled basis, because they consume infrastructure and can interfere with shared environments.

Automated thresholds should be tolerant of normal variation. Comparing one exact requests-per-second number can create noisy builds. Percentile limits, maximum error rates and a minimum achieved request rate are usually more useful. Store raw output and monitoring links as build artefacts, then investigate trends rather than treating every minor change as a defect.

Before a test reaches a shared or production-like environment, agree on ownership and safety limits. Notify the operations team, identify the source addresses, set a maximum rate and prepare a stop command. This is particularly important for APIs used by Australian customers during business hours, when an accidental stress test can affect real transactions and support teams.

Choosing between wrk and wrk2

The choice depends on whether the primary question concerns maximum sustainable throughput or latency at a controlled arrival rate. wrk is often the quickest way to establish a baseline and find the saturation point. wrk2 is preferable when queueing and tail latency matter, especially for services with strict response-time objectives.

Neither tool replaces system observability or a scenario engine. They are excellent for HTTP traffic, but they do not automatically model browser behaviour, message queues, long user journeys, WebSockets or complex token lifecycles. Use scripts for focused API workloads and a broader load-testing solution when the business process requires multiple coordinated actors.

Testing need Better fit Useful evidence
Quick maximum-throughput baseline wrk Requests per second, latency, errors
Latency at a fixed arrival rate wrk2 p95/p99 latency and achieved rate
Dynamic JSON and headers Either with Lua Status codes and response validation
Long business workflow A scenario-focused tool Step timings and transaction success
Capacity threshold discovery Both in stages Rate, saturation signals and failures
CI smoke performance check wrk or wrk2 Stable threshold and trend data

Use wrk first to understand the rough operating range, then use wrk2 to test selected rates around the expected limit. This two-stage method avoids wasting long constant-rate runs on a service that has not yet been configured correctly.

Turning results into an engineering decision

A benchmark becomes valuable when it changes a decision. If p99 latency rises sharply at 1,500 requests per second while CPU remains low, investigate locks, database pools, external services or application queues. If the load generator reaches its own CPU ceiling first, move it to a larger host and repeat before changing the API.

Document the capacity statement in operational terms: for example, “the service sustained 1,200 successful requests per second for ten minutes with p99 below 400 ms and fewer than 0.1% server errors.” Include the test conditions, because the result may depend on payload size, cache state, database scale and connection reuse.

For an Australian deployment, repeat important checks in the same cloud region used by customers, account for local peak periods, and ensure test records remain synthetic and segregated. A Melbourne result from a warm cache should not be presented as a nationwide user experience or as evidence that a Sydney database replica will behave identically.

The next practical step is to create one Lua script for a representative authenticated endpoint, run a 60-second wrk baseline, and record p95, p99, successful throughput and error rate before repeating the same workload with wrk2 at a fixed request rate.