Open-Source Wikis

/

nginx

/

Systems

/

Request lifecycle

nginx/nginx

Request lifecycle

Active contributors: Sergey Kandaurov, Maxim Dounin, Roman Arutyunyan

The state machine that every HTTP request, regardless of protocol version, runs through. Understanding it is the single highest-leverage thing to know about the HTTP code.

The 11 phases

src/http/ngx_http_core_module.h defines the enum:

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,
    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
} ngx_http_phases;

Each phase has a ngx_http_phase_handler_t array with checker + handler function pointers. checker decides whether to call handler and what to do with its return value; handler is the actual hook.

How modules attach to phases

In postconfiguration, an HTTP module appends a handler:

static ngx_int_t
ngx_http_my_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }
    *h = ngx_http_my_access_handler;
    return NGX_OK;
}

After all modules have registered, ngx_http_init_phase_handlers() flattens these arrays into a single cmcf->phase_engine.handlers[] array with explicit "next phase" indexes. At request time, the engine just walks this array.

What each phase does

POST_READ

Earliest hook after headers are parsed. The classic user is realip, which rewrites r->connection->sockaddr based on X-Forwarded-For / X-Real-IP so subsequent phases see the real client IP.

SERVER_REWRITE

Rewrites at server scope. The rewrite module attaches here for rewrite ... break; directives that aren't tied to a location.

FIND_CONFIG (core, implicit)

Walks the location tree to pick a matching location block. There's no module hook here — this phase is hard-coded in the engine. After this phase, r->loc_conf points at the chosen location's config. Modules' per-location configs become accessible.

The location tree is precomputed in a static (regex blocks aside) tree-of-arrays optimized for prefix matching with optional regex fallback. Code: ngx_http_init_locations, ngx_http_init_static_location_trees, ngx_http_core_find_location, all in ngx_http_core_module.c.

REWRITE

Rewrites at location scope. Combined with POST_REWRITE, this is where the rewrite module's compiled scripts (variable assignments, if blocks, rewrite directives) actually execute.

POST_REWRITE

If a rewrite changed r->uri, jumps back to FIND_CONFIG to re-pick a location. Limited to 10 internal redirects (r->internal_redirect_count) to prevent loops.

PREACCESS

Before access checks. limit_conn, limit_req, and degradation attach here. These can short-circuit with NGX_HTTP_SERVICE_UNAVAILABLE (or 429) without consulting any auth modules.

ACCESS

Authorization. access (allow/deny by IP), auth_basic, and auth_request attach here. The phase's checker understands the satisfy directive: with satisfy all, every handler must pass; with satisfy any, one passing handler is enough and the rest of the phase is skipped.

POST_ACCESS

Bookkeeping for satisfy any — finalizes whether any access handler passed.

PRECONTENT

Before the content handler. try_files and mirror attach here. try_files walks a list of file names and may set r->uri and short-circuit; mirror issues subrequests to the configured mirror locations. (PRECONTENT was once called TRY_FILES — same idea.)

CONTENT

The phase that produces the response body. Either a content handler is attached to the location (r->content_handler non-NULL, set by proxy_pass, fastcgi_pass, dav, etc.) or modules attached to the phase get called in order.

The handler returns a status code: NGX_HTTP_OK (we sent the response), NGX_HTTP_NOT_FOUND (try the next), NGX_DECLINED (not us, try next), NGX_DONE (we're handling it asynchronously, leave us alone), NGX_AGAIN (retry later). The engine maps these into either continuing or finalizing.

LOG

Per-request bookkeeping, run after the response has been sent. The access log module (ngx_http_log_module) attaches here to write a log line.

Read path: connection → request

sequenceDiagram
    participant E as event loop
    participant L as listener handler
    participant C as ngx_http_init_connection
    participant W as wait_request_handler
    participant P as parse_request_line
    participant H as parse_request_headers
    participant R as ngx_http_process_request

    E->>L: socket readable on listen fd
    L->>C: ngx_event_accept dispatched to per-protocol init
    C->>E: arm c->read for first byte
    E->>W: bytes available
    W->>P: read into recv_buf, parse first line
    P->>H: parsed; arm to read more headers
    H->>R: \r\n\r\n reached
    R->>R: ngx_http_init_phase_handlers walk

ngx_http_init_connection is in src/http/ngx_http_request.c. It:

  1. Allocates an ngx_http_connection_t (per-connection HTTP state).
  2. Sets c->read->handler = ngx_http_wait_request_handler.
  3. Arms a timer (client_header_timeout).
  4. Returns to the event loop.

When the first byte arrives, wait_request_handler allocates an ngx_http_request_t from r->pool, sets the read handler to ngx_http_process_request_line, and continues. Each handler is just an event handler; the event loop drives the whole thing one ready event at a time.

Body reading

For requests with a body (Content-Length or Transfer-Encoding: chunked), the body is not read during phase processing by default. The content handler decides:

  • Discard the bodyngx_http_discard_request_body(r) reads and tosses.
  • Read all of it into memory/temp filengx_http_read_client_request_body(r, post_handler). This buffers the body using client_body_buffer_size and possibly client_body_temp_path.
  • Stream it through — used by proxy_pass with proxy_request_buffering off; and by fastcgi_pass. The body is forwarded chunk by chunk as it arrives.

Code: src/http/ngx_http_request_body.c.

Output

After the content handler decides what to send, output flows through filters (see index) and ends in ngx_http_write_filter, which calls c->send_chain (typically sendfile-backed via ngx_linux_sendfile_chain or equivalent on other OSes).

If the socket is full, ngx_http_write_filter returns NGX_AGAIN and the request is paused until the write event fires. The engine handles this transparently — content handlers can ignore NGX_AGAIN from ngx_http_send_header and ngx_http_output_filter by checking r->buffered.

Finalize

Every code path ends in ngx_http_finalize_request(r, rc):

  • rc == NGX_OK / 200-class — flush remaining output, log, possibly enter keepalive
  • rc == NGX_HTTP_* — send canned error page (or run error_page redirection)
  • rc == NGX_ERROR — close the connection
  • rc == NGX_DONE — already handled, just clean up
  • rc == NGX_DECLINED — re-enter the phase engine

Subrequests finalize differently: their parents are notified via r->parent->postponed. The postpone filter merges output back in order.

Internal redirects

X-Accel-Redirect from a backend, error_page = /uri;, and try_files all use internal redirects: ngx_http_internal_redirect(r, &uri, &args) resets r->uri, increments r->internal_redirect_count, and re-enters the phase engine at FIND_CONFIG. This is bounded at 10 to prevent infinite loops.

Keep-alive

After a successful response, if Connection: keep-alive and keepalive_timeout allows it, the engine arms a new ngx_http_wait_request_handler and goes back to sleep. The same ngx_connection_t and c->pool are reused; r->pool is destroyed and a new one is created on the next request.

HTTP/2 and HTTP/3 streams

H2 and H3 multiplex many requests on one connection. The framing layer creates one ngx_http_request_t per stream and feeds it into the same phase engine. The differences:

  • Headers come from a frame parser (ngx_http_v2_state_*, ngx_http_v3_parse_*), not from ngx_http_parse_request_line.
  • Body bytes come from DATA frames, not from raw socket reads.
  • Output goes through ngx_http_v2_filter_module / ngx_http_v3_filter_module instead of the chunked filter.

The application logic (phase handlers, filters, content handlers) is identical between H1, H2, and H3.

Common pitfalls

  • Calling ngx_http_finalize_request twice — the second call is usually a bug and may double-free. Use r->main->count to check.
  • Forgetting to discard the body — if a content handler returns without reading or discarding, the connection state can corrupt on the next pipelined request.
  • Mixing subrequest output with parent output without the postpone filter — bypass it and you'll see interleaved bytes. Always go through ngx_http_output_filter.
  • Ignoring NGX_DECLINED vs NGX_OK — phase handlers must return the right code for the engine to advance.

For lower-level changes: the engine itself is in src/http/ngx_http_core_module.c's ngx_http_core_run_phases. Modify with extreme care; every HTTP module relies on its semantics.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Request lifecycle – nginx wiki | Factory