Nginx is renowned for its performance, but its internal architecture is equally elegant. Every HTTP request passes through a defined sequence of 11 phases, each handled by registered handler modules. Understanding how nginx processes a request at the source level reveals why nginx is so fast, so configurable, and so extensible. This post walks through that pipeline, referencing the nginx source (src/http/) at each stage.
Event-Driven Foundation
Before a request exists, nginx is waiting. The master process manages worker processes, each of which runs its own event loop (ngx_worker_process_cycle in ngx_process_cycle.c). Workers call ngx_process_events_and_timers(), which on Linux invokes epoll_wait(). When a new TCP connection arrives, the accept event fires, triggering ngx_event_accept() → ngx_http_init_connection().
No thread per connection, no fork per request — one worker process handles thousands of connections using this event loop. This is the architectural foundation for nginx’s concurrency.
Request Reading and Parsing
ngx_http_init_connection() sets up a timer and registers ngx_http_wait_request_handler as the read event handler. When the client sends the request line, this handler allocates an ngx_http_request_t struct (the central request object) and calls ngx_http_parse_request_line() — a hand-optimized state machine in ngx_http_request.c that parses the HTTP request line with zero allocations.
Headers are then read and parsed by ngx_http_process_request_headers(). Each header is stored as an ngx_table_elt_t in the request’s headers_in linked list. Nginx does not use a hash map for headers during parsing — it uses a hash table only for known headers (Content-Type, Host, etc.) via ngx_http_headers_in[].
The 11-Phase Pipeline
After headers are parsed, nginx runs the request through phases via ngx_http_core_run_phases(). Phases are defined in ngx_http_core_module.c. The 11 phases in order are: NGX_HTTP_POST_READ_PHASE, NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_POST_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACCESS_PHASE, NGX_HTTP_POST_ACCESS_PHASE, NGX_HTTP_PRECONTENT_PHASE, NGX_HTTP_CONTENT_PHASE, NGX_HTTP_LOG_PHASE.
Each phase has a list of handler functions registered by modules. nginx calls them in order. A handler returns NGX_OK (done), NGX_DECLINED (pass to next handler), or NGX_AGAIN (suspend, waiting for I/O). This is how modules like ngx_http_rewrite_module, ngx_http_access_module, and ngx_http_proxy_module cooperate without knowing about each other.
FIND_CONFIG_PHASE: Location Matching
FIND_CONFIG_PHASE is handled exclusively by ngx_http_core_find_config_phase(). It matches the request URI against the location blocks using a combination of exact match, prefix match (stored in a sorted array for binary search), and regex match (processed in declaration order). The matched location’s configuration is stored on the request as ngx_http_core_loc_conf_t, affecting all subsequent phases.
ACCESS_PHASE: Auth Modules
The ACCESS_PHASE is where ngx_http_access_module (IP allow/deny) and ngx_http_auth_basic_module live. The satisfy directive controls whether all handlers must pass (satisfy all) or just one (satisfy any). The phase checker (ngx_http_core_access_phase) enforces this logic between handler calls.
→ Related: Nginx gzip + character encoding edge cases (Blog 07)
CONTENT_PHASE: Generating the Response
The CONTENT_PHASE is where the actual response is generated. The location’s content handler (set via ngx_http_core_loc_conf_t.handler) is called directly — bypassing the phase runner. For proxy_pass, this is ngx_http_proxy_handler(). For static files, it is ngx_http_static_handler(). For FastCGI, it is ngx_http_fastcgi_handler().
Content handlers build the response by creating ngx_chain_t linked lists of ngx_buf_t buffers — a zero-copy I/O model. Buffers may reference memory or file regions (sendfile-compatible). These chains are passed to ngx_http_output_filter(), which runs through the output filter chain (gzip, SSI, charset conversion, etc.) before hitting the network.
LOG_PHASE and Finalization
After the response is sent, LOG_PHASE fires. ngx_http_log_module writes the access log here. Then ngx_http_finalize_request() is called, which decrements the request reference count and, when it reaches zero, calls ngx_http_free_request() to free memory from the request pool and close the connection (or return it to keepalive).
Conclusion
Understanding how nginx processes a request transforms your ability to debug, configure, and extend it. The 11-phase pipeline, the zero-copy buffer chain model, and the event-driven foundation are not just implementation details — they are the reason nginx can saturate a 10Gbps NIC from a single process. Reading src/http/ngx_http_core_module.c and src/http/ngx_http_request.c with this map will make the rest of the codebase legible.


Leave a Reply