Open-source project
wg/wrk avatar
wg/wrk

wrk links LuaJIT unconditionally and takes its version from git describe

GitHub describes it as Modern HTTP benchmarking tool. The repository metadata lists C as its primary language. The metadata lists the NOASSERTION license. This article stays within the project description and details documented in the GitHub repository README.

40,418 stars3,030 forksCNOASSERTION

At a glance

What is it?
wrk is a small C HTTP load generator built around threads, connections and an event loop. Its Makefile is more informative than its README: it links LuaJIT whether or not you script, branches for four operating systems and none for Windows, vendors its own OpenSSL, and has had no commit since 2023-12-30.
Who is it for?
wrk fits someone who needs a fixed thread and connection load against one HTTP endpoint and can satisfy the ephemeral port and listen backlog conditions first. It does not fit a team that needs a stream of published releases, or one that wants a scripting language other than LuaJIT.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Probably not. The repository last received commits 33 months ago, on December 30, 2023.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The README calls LuaJIT optional, the Makefile links it anyway

The README describes an optional LuaJIT script for HTTP request generation, response processing and custom reporting. Optional describes how you use it, not whether the build needs it. The root Makefile appends -lluajit-5.1 to LIBS unconditionally, and it has a rule that generates obj/bytecode.c from src/wrk.lua by running the LuaJIT compiler over it. There is no configure flag that drops the dependency.

The same pattern applies to TLS. ssl.c is in the source list, -lssl and -lcrypto are in LIBS, and the default path is to build a static library into obj/lib rather than use the system one. Two environment variables change that: set WITH_LUAJIT or WITH_OPENSSL and the Makefile compiles against your existing install instead, with the include and library paths added and the vendored build skipped.

For a first build that means a compiler, plus LuaJIT and OpenSSL, either supplied by you or compiled from the archives in deps/ into an obj directory. The INSTALL file at the top level is where the project puts the build instructions.

uname picks the branch, and four systems get extra flags

The Makefile derives TARGET from uname -s, lowercased, and then branches on it. Four systems have their own handling. On sunos it adds -D_PTHREADS and -D_POSIX_C_SOURCE=200112L and links -lsocket. On darwin it exports MACOSX_DEPLOYMENT_TARGET from sw_vers. On linux it adds -D_POSIX_C_SOURCE=200112L with -D_BSD_SOURCE and -D_DEFAULT_SOURCE, links -ldl and passes -Wl,-E. On freebsd it adds -D_DECLARE_C99_LDBL_MATH and passes -Wl,-E.

Everything else gets the base set: -std=c99, -Wall, -O2 and -D_REENTRANT, linked against -lm, -lssl, -lcrypto and -lpthread. That is a meaningful difference rather than a cosmetic one, because the Linux branch is what defines the BSD and default source macros the code compiles against.

There is no Windows branch anywhere in that conditional, and TARGET falls back to the literal unknown when uname is not there. So a Windows machine has no native build path in this Makefile and needs a POSIX environment, which is the same answer the search traffic around Windows builds keeps arriving at. The ten source files are wrk.c, net.c, ssl.c, aprintf.c, stats.c, script.c, units.c, ae.c, zmalloc.c and http_parser.c.

The version is generated at build time and there are no tags to describe

The version string is not stored in a file. VER is defined as the output of git describe --tags --always --dirty, and a build rule pipes it into a C file, writing a const char pointer that gets linked into the binary. A checkout with tags gets a tag name, a checkout without them gets a commit hash, and a modified tree gets the dirty suffix.

This repository publishes no GitHub releases, so a clone of master has nothing to describe, and a source archive that is not a git repository has no git at all. Two people building the same commit can therefore end up with different version strings, and neither of those strings tells you which release you are running, because there is no release.

The freshness signal is the branch itself. The last push to master was on 2023-12-30, and the top level has a CHANGES file rather than a changelog generator. Treat the source tree as the only upgrade path and read CHANGES before assuming a local patch is current.

Ephemeral ports and the listen backlog decide whether a number means anything

The benchmarking tips section is two paragraphs and both of them are caveats about the load generator rather than the server. The machine running wrk must have a sufficient number of ephemeral ports available, and closed sockets should be recycled quickly. Separately, the server's listen(2) backlog should be greater than the number of concurrent connections being tested, so the initial connection burst has somewhere to land.

Read that as a contract. A wrk result is a statement about the server only when both sides are configured for it. Run out of ephemeral ports and the client becomes the bottleneck, and the throughput number you report describes the load generator. A backlog smaller than the connection count means the measurement starts with a queue the server had to absorb, which shows up in the latency column and nowhere else.

Nobody has to guess what the reference run looked like, because the README prints one. It shows 12 threads and 400 connections over 30 seconds, 22464657 requests in 30.00s, 17.76GB read, 748868.53 requests per second and 606.33MB transfer per second, with average latency of 635.91us and a standard deviation of 0.89ms against a 12.92ms maximum.

Scripts that inspect responses cannot be compared with scripts that do not

The same section states the cost of scripting before you find it yourself. A user script that only changes the HTTP method, the path, adds headers or a body will have no performance impact. Per-request actions, particularly building a new HTTP request, and any use of response() will necessarily reduce the amount of load that can be generated.

That is a hard boundary on what a benchmark means. A script that rewrites the URL per request can be compared with a plain run. A script that reads a token out of a response and puts it in the next request cannot, because the client is now doing work per request and the requests per second figure is measuring that work as much as the server. Mixing the two in one report is the most common way an otherwise careful benchmark ends up meaningless.

The alternative approach, for what it is worth, is a load tool whose scripting language is JavaScript rather than LuaJIT and whose load model is built around ramping stages instead of a fixed connection count. That changes the shape of the report rather than the principle: whatever generates the load has a per-request budget, and the README here is unusually direct about spending it.

The Stdev column is the part of the output worth reading first

wrk's output is a per-thread table, and the interesting numbers are in the spread, not the average. The reference output has four columns, Avg, Stdev, Max and +/- Stdev, and it fills two rows. Latency reads 635.91us average, 0.89ms standard deviation and a 12.92ms maximum at 93.69 percent. Req/Sec reads 56.20k average, 8.07k standard deviation and a 62.00k maximum at 86.54 percent.

A high value in the last column means the threads were not contributing evenly, which points at the client or at the connection distribution rather than at the server. And the distribution is yours to choose: -c sets the total number of connections and each thread handles N equal to connections divided by threads. With the reference run's 400 connections over 12 threads that quotient is not a whole number, and the README does not say how the remainder is spread, so a run with an awkward ratio deserves a second look before its average is quoted.

The --latency option exists to print the detailed statistics, and --timeout records a timeout when a response is not received within the time you set.

The licence file is not the whole picture, and the binary is export classified

wrk is a small program assembled from other people's code, and it says so. The acknowledgements name the ae event loop from redis, the http-parser from nginx, joyent and node.js, and Mike Pall's LuaJIT, and they direct you to the NOTICE file for licensing details. The top level holds LICENSE and NOTICE as separate files for that reason, so adopting wrk means reading both.

There is a second document that organisations tend to miss. The cryptography notice states that the distribution includes cryptographic software, that the US Department of Commerce Bureau of Industry and Security has classified it as Export Commodity Control Number 5D002.C.1, and that the form of the distribution makes it eligible under the License Exception ENC Technology Software Unrestricted at Section 740.13 of the Export Administration Regulations, for both object code and source. It points at the Wassenaar arrangement for the general background.

The practical effect is that a build which links OpenSSL inherits an export classification whether or not you touch TLS. That is a compliance fact to pass to whoever owns export control in your organisation, and the README gives you the classification number rather than making you derive it from the linked libraries.

Editorial conclusion

wrk fits someone who needs a fixed thread and connection load against one HTTP endpoint and can satisfy the ephemeral port and listen backlog conditions first. It does not fit a team that needs a stream of published releases, or one that wants a scripting language other than LuaJIT. Verify what you are installing before anything else, because the last push to master was on 2023-12-30, no GitHub release exists, and the version string inside a local build is generated by git describe rather than taken from a tag.

Frequently asked questions

How do I run a wrk benchmark?

The README's basic usage is wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html, which runs for 30 seconds with 12 threads and 400 open connections. The options are -c for connections, -d for a duration such as 2s, 2m or 2h, -t for threads, -H for a header to add, --latency for detailed latency statistics and --timeout for a response deadline.

What does wrk need installed before it can be built?

A C toolchain plus LuaJIT and OpenSSL, which the Makefile links and builds from the archives in deps/ into an obj directory unless WITH_LUAJIT or WITH_OPENSSL point at an existing install. Platform flags are set for sunos, darwin, linux and freebsd, and the INSTALL file at the top of the repository has the build instructions.

What scripting language does wrk use?

LuaJIT. The README says an optional LuaJIT script can perform HTTP request generation, response processing and custom reporting, with the details in the SCRIPTING file and examples in scripts/. The Makefile links -lluajit-5.1 and generates obj/bytecode.c from src/wrk.lua, so the dependency exists even without a script.

Is wrk still maintained?

The last push to the master branch was on 2023-12-30 and the repository publishes no GitHub releases. A CHANGES file at the top level records what changed, and the version string inside a local build is generated by git describe, so there is no tagged version to install.

Official sources

  1. Official README
  2. Project repository