Vivian Voss

Technical Beauty: nginx

nginx architecture unix

Technical Beauty ■ Episode 03

In 1999, a software engineer named Dan Kegel published a web page titled “The C10K Problem.” The question was straightforward: can a single server handle ten thousand concurrent connections? The answer, for every web server in production at the time, was no. Not “no, the hardware is too slow.” The hardware was fine. The answer was “no, the architecture is wrong.”

Five years later, a Russian developer named Igor Sysoev published a web server that answered the question with a yes, a shrug, and rather elegant C.

The Problem

Apache was the web server. In 1999, it held over 60 per cent market share and deserved most of it. The httpd project had built reliable, well-documented, extensible server software that ran the early web. The problem was not quality. The problem was concurrency.

Apache’s original model, the prefork MPM, spawned one process per connection. A process with its own memory space, its own file descriptors, its own stack. For fifty concurrent users, this was perfectly reasonable. For five hundred, it was expensive. For ten thousand, it was arithmetic: ten thousand processes, each consuming several megabytes of RAM, each requiring a context switch from the kernel scheduler. The server did not run out of bandwidth. It ran out of processes. Or, more precisely, it ran out of the fiction that processes are free.

The worker MPM improved matters by using threads instead of processes. Threads share memory. Threads are lighter. But a thread still represents a dedicated execution context for a single connection, and a connection that is waiting for the client to send the next byte is a thread that is doing nothing whilst occupying a scheduler slot. The web is not computation-bound. The web is I/O-bound. Most of the time, a web server is waiting. Apache assigned a thread to each act of waiting. nginx assigned zero.

Connection Handling: Two Architectures Apache (prefork / worker) One thread per connection. Blocked threads shown in grey. T1 idle T2 idle T3 work T4 idle T5 idle T6 idle T7 work T8 idle 8 connections = 8 threads. 6 idle, burning memory. 10,000 = collapse. Each thread: ~8 MB stack. 10K threads = 80 GB. The maths is unkind. nginx (event-driven) One process, one event loop. Connections multiplexed via epoll/kqueue. Event Loop epoll / kqueue C1 C2 C3 ... C4k ... C8k ... C10k 10,000 connections. One process. No idle threads. No wasted memory. Apache asked: how many threads can we afford? nginx asked: why do we need threads at all?

The Man

Igor Sysoev was a system administrator in Moscow. Not a computer scientist at a university. Not a researcher at a corporation. A sysadmin who ran Rambler, one of the largest Russian-language internet portals, and watched Apache buckle under the load. He began writing nginx in 2002. He released it publicly in October 2004. The name stands for nothing in particular (it is sometimes expanded to “engine x” but Sysoev has been charmingly non-committal about this). The pronunciation is “engine-x,” not “en-jinx,” though one suspects the latter would have been rather apt for Apache’s market share.

Sysoev did not build nginx to compete with Apache. He built it because Apache could not serve Rambler’s traffic. The motivation was not ideological. It was operational. The most enduring software tends to begin this way: someone has an actual problem, writes an actual solution, and discovers that the problem is universal.

The Architecture

nginx’s architecture is an event loop. A single worker process (or a small, fixed number of workers, typically one per CPU core) handles all connections. When a connection arrives, the worker registers it with the operating system’s event notification mechanism: kqueue on FreeBSD, epoll on Linux. The worker then moves on. It does not wait. It does not block. It does not allocate a thread to sit idle until the client sends another byte. When data arrives on any registered connection, the kernel notifies the worker, which processes the data and moves on again.

This is not a novel idea. Event-driven I/O existed long before nginx. The reactor pattern was documented in the 1990s. What Sysoev did was apply it to a production web server with the discipline to avoid the traps that had ensnared earlier attempts. No callbacks into user-supplied modules that might block. No thread pools hidden behind the event loop “just in case.” No abstraction layers that reintroduce the overhead the event model was meant to eliminate. Pure, uncompromising non-blocking I/O from the socket to the response.

The result: a single nginx worker process can handle tens of thousands of simultaneous connections with a memory footprint measured in megabytes. Not gigabytes. Megabytes. An Apache prefork configuration serving the same number of connections would require a thread count that exceeds the kernel’s scheduler limits before it exceeds the available RAM. The C10K problem was not solved by faster hardware or cleverer thread pools. It was solved by refusing to create threads in the first place.

The C

nginx is written in C. Not C++. Not Rust. Not Go. C. The language that trusts you with pointers, expects you to manage your own memory, and offers precisely zero opinions about how you structure your programme. Sysoev chose C because C is the language of systems programming, and a web server is a systems programme that happens to speak HTTP.

The codebase is clean. Not “clean for C” (the qualifier that damns with faint praise). Clean. The naming conventions are consistent. The memory management is explicit and auditable. The module system is a set of function pointers and configuration callbacks, not a framework. There is no runtime. There is no garbage collector. There is no interpreter. There is a binary, a configuration file, and the operating system. The dependency list is: libc, and whatever TLS library you choose to compile against. That is the entire supply chain.

The Numbers

According to Netcraft surveys, nginx serves approximately 34 per cent of all websites globally. Apache has declined to roughly 30 per cent. The crossover happened in the mid-2010s and the gap has widened since. This is not a story of marketing. nginx had no marketing budget. It had no venture capital (until much later). It had no corporate sponsor. It had a binary that handled more connections with less memory, and word travels in operations teams.

Web Server Market Share (active sites) Source: Netcraft surveys, 2004–2025 70% 55% 40% 25% 10% 0% 2004 2008 2012 2016 2020 2025 crossover nginx ~34% Apache ~30% No marketing budget. No venture capital. Just fewer context switches.

In 2011, Sysoev co-founded Nginx, Inc. to provide commercial support and develop NGINX Plus. In 2019, F5 Networks acquired the company for $670 million. Six hundred and seventy million dollars for a web server written by a sysadmin in Moscow who was irritated by Apache. One suspects the return on irritation has rarely been higher.

The Roles

nginx is described as a web server, which is accurate in the same way that describing a Swiss Army knife as a blade is accurate. nginx serves static files. It also operates as a reverse proxy, forwarding requests to upstream application servers. It operates as a load balancer, distributing traffic across multiple backends. It operates as an HTTP cache, a TLS terminator, a mail proxy, a gzip compressor, a rate limiter, and a WebSocket proxy. All from the same binary. All configured in the same file. All running in the same event loop.

The typical production deployment in 2025 does not use nginx to serve static files to end users (though it does that superbly). It uses nginx as the front door: terminating TLS, compressing responses, caching assets, rate-limiting abusive clients, and proxying clean requests to whatever application server sits behind it. The application server speaks to the database. nginx speaks to the internet. The division of labour is clean, and nginx handles its side of the arrangement with the quiet efficiency of a butler who has seen everything and is surprised by nothing.

The Configuration

nginx’s configuration language is its own invention. Not YAML. Not JSON. Not XML. Not TOML. A bespoke syntax of directives and blocks that reads like a cross between a shell script and a declaration of intent:

server {
    listen       80;
    server_name  example.com;

    location / {
        proxy_pass  http://backend;
    }
}

Seven lines. A complete reverse proxy. The configuration is declarative: you state what you want, not how to achieve it. There are no loops, no conditionals (well, there is if, but the official documentation famously warns that “if is evil”), no imperative logic. This is not a programming language. It is a description of the server you want, written in a syntax that happens to be machine-readable. The machine reads it once at startup and does precisely what you described.

The module system follows the same philosophy. nginx modules are compiled into the binary (or, since 1.9.11, loaded dynamically). They are not runtime plugins. They are not scripts. They are C code that registers callbacks at specific phases of request processing. The overhead of a module is the overhead of a function pointer dereference. In a world where “extensibility” typically means “an interpreter embedded in your server,” nginx’s approach is refreshingly mechanical.

The Event MPM Postscript

Apache did eventually address the C10K problem. The Event MPM, introduced experimentally in Apache 2.2 and stabilised in 2.4, uses a hybrid model: an event loop for keep-alive connections, threads for request processing. It is a genuine improvement. It handles concurrency far better than prefork or worker ever did. It arrived in 2012.

By 2012, nginx had been in production for eight years. It had already solved the problem. It had already proven the architecture. It had already captured the mindshare of every operations engineer who had benchmarked both servers and noticed that one of them used a fraction of the memory. Apache’s Event MPM is the right answer, delivered after the exam has been marked and the results published. Technically correct. Strategically irrelevant. The web had moved on, and it had moved on behind nginx.

The Philosophy

What makes nginx technically beautiful is the same thing that makes sed, curl, and WireGuard beautiful: a root-cause analysis followed by a root-cause elimination. Sysoev did not optimise Apache. He did not write a faster thread scheduler. He did not propose a patch to the prefork MPM that squeezed another ten per cent from the existing architecture. He looked at the architecture, identified the structural flaw (one execution context per connection), and built a server that did not have it.

This is the difference between optimisation and architecture. Optimisation makes the wrong thing faster. Architecture replaces the wrong thing with the right thing. Apache spent a decade optimising thread pools. nginx spent 2002 to 2004 eliminating them. Twenty years later, the event-driven model is not merely the dominant architecture for web servers. It is the dominant architecture for network software: Node.js, HAProxy, Envoy, Caddy (via Go’s goroutines, which are green threads on an event loop). Every modern proxy, every modern load balancer, every modern API gateway uses the pattern that nginx popularised. Not because Sysoev invented the event loop. Because he proved, at scale, in production, on one of Russia’s busiest websites, that it was the correct architecture for serving the web.

Pure C. No framework. One binary. Event-driven since 2004. The C10K problem was never a hardware problem. It was an architecture problem. Sysoev solved it by refusing to allocate a thread for the act of waiting. Twenty years later, the industry agrees. Root-cause elimination beats optimisation. Every time.