Jul 8, 2026

ngx_l402 primitives: nginx

Introduction to nginx

A few months ago I was trying to explain ngx_l402 to a friend (non-tech). Ten minutes in I realized I was doing that thing where you explain the cool part of a project without explaining the foundation. I was talking about macaroons and Lightning invoices and he was still confused about the underlying primitives (nginx and L402) of ngx_l402.

So when I started to write this piece it made me think whether a person who does not have much understanding of nginx and l402 will understand the ngx_l402 module. Probably not. So, I decided to write a series of blogs instead.

This article is the first in a series on nginx, L402, and the ngx_l402 module. It covers nginx itself. L402 and ngx_l402 come in the next posts. Here I try to give an idea about what nginx is, the historical context of the project, and how its architecture makes module development possible.

Note: I have tried to explain the concepts behind some terms a beginner might encounter for the very first time that are relevant to understanding the concepts this article tries to explain. But for the other terms, I have left links to external resources for the reader to peruse at their own will.


What is nginx?

Figure 1: nginx

Most of the content you interact with online is served to your browser from servers. Those seemingly never-ending racks stacked with black boxes that have LEDs blinking on them like you’ve seen in movies and TV shows. But those (physical) servers need software that makes the process of fetching and storing data in them work. This piece of software is called… server. I know, very creative.

nginx (pronounced “engine-x”) is one such server. It is an open-source web server. It is not just just that, it is also a reverse proxy, a load balancer, a mail proxy, a cache, and a few other things. At its center, though, it is a web server.

Igor Sysoev, a Russian software engineer, started writing it in 2002 while working at Rambler (this company was largely similar to what Yahoo! was in the West at the time). He released it publicly on October 4, 2004.

Sysoev was not trying to build the most popular web server on the internet (if you didn’t know this yet, I apologise for the spoiler). He was trying to fix a specific problem at work. We will visit the problem he was trying to solve later in this article.


Ummm.. is it a server, load balancer, or reverse proxy?

Yes.

It is all three.

As a web server, nginx serves static files (HTML, CSS, images) quickly. As a reverse proxy, it sits in front of application servers (Node, Python, Ruby, whatever) and forwards requests to them. Most people use nginx this way now: it handles TLS, compression, and connection management so the backend can focus on actual work. As a load balancer, it spreads traffic across multiple backend instances using round-robin, least-connections, or IP-hash strategies.

It also does mail proxying, caching, API gateway duties, rate limiting, WebSockets, and gRPC termination. The same process serving your static files might be terminating a gRPC stream, caching a GraphQL response, and enforcing an access policy.


The 10K problem and how nginx solved it

Remember the problem that Sysoev was trying to solve sitting in his basement (actually I don’t know if he was sitting in his basement, but it sounds nerdy and cool, so why not)?

Back to that problem now.

A connection, in this context, is just a TCP link between a client (usually a browser) and the web server. When you type a URL and hit enter, your browser opens a connection to the server, asks for the page and once it gets the response, it closes the connection. In the early 2000s, Apache HTTP Server ran most of the web. It spawned a new process (if you aren’t aware of what a process, keep reading, I have explained it below) or thread for every incoming connection. That worked fine for a few hundred connections. It even worked for a few thousand. But once sites started pushing toward 10,000 (hence the name 10K) concurrent connections, performance bricked.

This is the C10K problem (the C here stands for concurrency). To understand why it breaks Apache, you need to know two things about how computers run programs.

Figure 2: Apache creates a process or thread per connection; nginx handles thousands of connections per worker through an event loop.

A process is a running instance of a program. If you open two browser windows, you have two browser processes. A thread is a smaller worker inside a process. A single process can run many threads, and threads are lighter than processes, but they still need their own bookkeeping.

Apache could run in different modes. In one mode, it created a new process for every connection. In another, it created a new thread for every connection. Either way, the idea was the same: one connection got one dedicated worker. With 10,000 concurrent connections, Apache had 10,000 processes or threads waiting in line.

A CPU core can only run one process at a time. So the CPU constantly pauses one process or thread, saves where it was, and loads another. That switching itself takes time. Every process also needs its own chunk of RAM for its data and open files. The operating system kernel has to track all of that memory, which adds even more overhead.

So the CPU is not serving requests. It is busy switching between processes and bookkeeping memory usage. That is the core of the C10K problem.

Sysoev removed this process-per-connection model. nginx uses an event-driven, asynchronous, non-blocking architecture instead.

A small number of worker processes run event loops. Each worker watches thousands of connections at once using kernel mechanisms like epoll on Linux or kqueue on BSD. When data is ready, the kernel notifies the worker, which handles it and moves on. No process sits blocked waiting for a slow client.

A single nginx worker can handle thousands of connections at once. Apache would need a separate process or thread for each of those connections, so the same load means thousands of workers instead of a few. The memory difference is just as stark. nginx often sits at tens of megabytes. Apache under the same load can easily climb into gigabytes.

Modern nginx deployments handle hundreds of thousands of concurrent connections on commodity hardware.


Adoption and market share

You should skip this section if you are here to learn about the tech and market share isn’t something you care about.

Nginx web server market share distribution

Figure 3: Nginx web server market share distribution

nginx went from a side project at a Russian portal to running a large part of the web. W3Techs puts it at roughly 32% of all websites whose server is known. Among the top 1,000 and top 10,000 sites, the share is higher. Netcraft’s surveys show nginx either leading or close behind in active sites.

Apache dominated for over two decades but has been steadily losing ground, down to about 23% per W3Techs. Its process-based architecture does not fit modern high-concurrency, cloud-native workloads as cleanly. Other players include Cloudflare’s server stack, LiteSpeed, Microsoft IIS, and newer entrants like Caddy.

Netflix, WordPress.com, and Airbnb all run nginx (I have added links that talk about this in the Resources section at the bottom of this article). In 2011, Sysoev co-founded NGINX Inc. to sell commercial support. In 2019, F5 Networks bought the company for roughly $670 million.


How nginx is architected

To understand nginx’s architecture, it helps if you know two terms: the master and worker processes. The master process is the main process that controls everything. It reads the configuration, binds to network ports, and manages the other processes. Worker processes are the ones that actually handle client connections and requests.

nginx master-worker architecture. The master manages worker processes; each worker runs an event loop and handles I/O via kernel notifications.

Figure 4: nginx master-worker architecture. The master manages worker processes; each worker runs an event loop and handles I/O via kernel notifications.

nginx starts one master process and one or more worker processes.

The master reads the configuration, binds to ports, and manages the workers. It never touches client connections directly. When you reload nginx, the master spawns new workers with the updated config and gracefully kills the old ones.

The workers do everything else. Each is a single-threaded process running an event loop. They accept connections, read requests, process them, and write responses.

Event-driven means the worker does not sit around waiting for one specific connection. Instead, it asks the operating system to notify it when any connection has something to do: new data arrived, a client is ready to read, a backend response came back. The worker handles whatever event happened, then goes back to waiting. Because the worker is single-threaded, it does not need locks to protect shared data, and the CPU does not waste time switching between threads.

If caching is on, you might also see a cache loader and cache manager process.

Master orchestrates. Workers do I/O. There is no shared state between workers, no complex locking, no thread pools.


Everything is a module

Everything in nginx is a module

Figure 5: Everything in nginx is a module

Just as everything in Linux is a file, everything in nginx is a module.

Almost everything nginx does lives in a module. The core is deliberately small. HTTP handling is a module. SSL/TLS is a module. Gzip (a compression format that makes responses smaller before sending them) is a module. Load balancing algorithms are modules. Even the event loop mechanism (epoll, kqueue, select) is pluggable. You get the idea.

Developers can extend nginx without touching core code. You compile your module, load it as a shared object, and it registers for specific phases of request processing: access control, content generation, logging, header filtering. nginx calls your code at the right moments.

This matters for security (especially in the Mythos era). You run only the code you need. You do not pay complexity costs for features you do not use. And when you do add a module, you are building on code that has survived two decades of internet-scale use.

There is a software saying (I don’t know if this is actually a saying, but you can just say things, I guess): use Lindy code. Lindy code is code that has been around long enough to have had its bugs found and fixed. nginx is about as Lindy as it gets.


How a request flows through nginx

Say a client makes an HTTP request to an nginx server running several modules. Here is what happens.

Figure 6: An HTTP request passes through nginx phases as a pipeline. The access phase (where ngx_l402 lives) can deny the request with 401, 403, or 402 before it ever reaches the backend.

The worker’s event loop notices a listening socket is readable and accepts the new connection. The HTTP module parses the request line and headers. Modules registered for the post-read phase can inspect or modify the request now.

nginx matches the Host header and URI against the configuration to find the right server and location block. Then the access phase runs. This is where IP restrictions, authentication, rate limiting, and L402 payment verification happen. If a module denies access, nginx returns 401, 403, or 402, and the request ends.

If access is granted, the content phase runs. nginx might read a static file, proxy to a backend, run FastCGI, or execute a custom content handler. Then response headers and body pass through filter modules: gzip, image transforms, header injection. Logging modules record the transaction. Finally, the response goes to the client, and the connection either closes or stays alive for the next request.

Each phase is a hook. A module registers a handler for whatever phase it needs. ngx_l402 registers an access-phase handler that checks for a valid L402 macaroon and payment preimage. If the macaroon or preimage is missing or invalid, it returns 402 Payment Required with a fresh challenge. If everything checks out, it steps aside and lets the request through.


Writing nginx modules in Rust

For most of nginx’s history, module development meant C. nginx is written in C, and the module API is a C API. ngx_l402 is written in Rust instead, using FFI (Foreign Function Interface ).

Rust compiles to a shared library that exports C-compatible symbols. nginx loads this library as a dynamic module and calls into it exactly like a C module. The Rust code calls back into nginx through FFI bindings.

F5 officially maintains the ngx-rust project, which provides Rust bindings for nginx module development. It splits into two crates:

  • nginx-sys: bindgen-generated FFI bindings from nginx’s C headers. It downloads nginx source and produces Rust equivalents of structs, enums, and functions.

  • ngx: higher-level Rust glue, traits, and APIs that wrap the raw FFI into something idiomatic. Module authors implement HTTPModule to register handlers.

A minimal Rust module looks roughly like this:

use ngx::{core, http};

struct MyModule;

impl http::HTTPModule for MyModule {
    type MainConf = ();
    type SrvConf = ();
    type LocConf = MyConfig;

    unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t {
        let htcf = http::ngx_http_conf_get_module_main_conf(cf, &ngx_http_core_module);
        let h = ngx_array_push(
            &mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers,
        ) as *mut ngx_http_handler_pt;
        if h.is_null() {
            return core::Status::NGX_ERROR.into();
        }
        *h = Some(my_access_handler);
        core::Status::NGX_OK.into()
    }
}

The module implements HTTPModule, registers an access-phase handler, and from there can inspect requests, modify responses, or deny access.

The Rust code runs directly inside the nginx worker. There is no sandbox. You get Rust’s memory safety and borrow checker, but you inherit nginx’s constraints. If your code panics, the worker dies. If your code blocks the event loop, every connection on that worker stalls. It is a powerful combination, but you have to respect the runtime.

ngx_l402 uses this Rust-to-nginx bridge to implement L402 payment logic with Rust’s safety guarantees while running at native speed inside one of the most widely used web servers on the internet.


What’s next

The next post will cover L402 itself: the protocol, macaroons and caveats, the Lightning payment flow, and why it matters for machine-to-machine payments.

After that, we will dig into ngx_l402.

By the end of the series, you should understand what ngx_l402 is trying to achieve.


Resources


If you spot errors or have questions/feedback, please feel free to reach out.

Thanks to

for proofreading this article.