namalzi

← All posts

Digging into nginx's Source 5: Modules and Directives — How One Line of nginx.conf Becomes a Module's Config Struct

nginx · Published

An nginx module is a single ngx_module_t struct, and a directive is one row in the ngx_command_t table that struct points to. We follow the module array configure generates, dynamic modules read with load_module, ngx_conf_handler() finding a function by directive name, ngx_http_block() creating per-module config structs for every http, server and location, and the merge that finishes once, at config time. We build a dynamic module with four directives to watch the callback order, the merged values and the error messages.

What this post covers

This is the fifth post in the "Digging into nginx's Source" series. In part 4 we followed main() into ngx_init_cycle(), where ngx_conf_parse() chops nginx.conf into words (tokens). This post picks up right after that: we follow a line such as greet_message "hi"; and see which module's function it reaches, and which field of which struct it ends up in.

Four questions:

Item Details
Target nginx
Version nginx 1.31.9 (master, development)
Commit 9a7439fd79 (2026-10-23)
Main files src/core/ngx_module.[ch], src/core/ngx_conf_file.[ch], src/core/nginx.c (load_module), auto/modules, src/http/ngx_http_config.h, src/http/ngx_http.c, src/http/ngx_http_core_module.c

All line numbers refer to this commit. Part 1 shows how to check out the same commit and build the Docker image used for the examples. The config and module code repeats itself in the same shape for events, stream and mail besides HTTP, so this post sticks to the main path through core and HTTP. What I left out is listed by name and location in "Going deeper" at the end.

On the map from part 2, this post zooms into the modules·directives box in zone A. Preparation.

Where this post sits on the series map. Zone A, "Preparation", holds startup·cycle (part 4), modules·directives (part 5, this post) and memory pools·data structures (part 3). Zone B, "Processes and events", holds master·workers (part 6) and the event loop (part 7). Zone C, "One HTTP request", holds reading the request (part 8), phases·location (part 9), filters·static files (part 10), upstream·proxy (part 11) and load balancing·keepalive·stream (part 12). Only this post's box is blue. The blue box is this post. Part 4 is about "reading" the config file; this post is about "handing" what was read to the modules.

Terms to know first

A module is the unit of almost every feature in nginx. HTTP processing itself (ngx_http_core_module), static files (ngx_http_static_module), proxying (ngx_http_proxy_module) — even the events {} block and the error_log directive are modules. In the source, a module is one struct variable of type ngx_module_t. Not a class or a plugin object, just a C struct holding function pointers and tables. If C structs and function pointers feel rusty, httpd part 2: Before Reading a Server Written in C — Structs, Function Pointers, Macros is a good warm-up.

The module type is the type field of ngx_module_t: NGX_CORE_MODULE, NGX_EVENT_MODULE, NGX_HTTP_MODULE, NGX_STREAM_MODULE, NGX_MAIL_MODULE and so on. The values are four-letter strings such as "CORE", "EVNT" and "HTTP" read as integers (we saw this in part 2).

The module context is the callback struct that the ctx field of ngx_module_t points to, and its shape depends on the type. For an HTTP module it is ngx_http_module_t (8 callbacks); for a core module it is ngx_core_module_t (2 callbacks). "ctx" means several different things in nginx's source, which is confusing; in this post I call this one the "module context" and the ngx_http_conf_ctx_t below the "config context struct".

A directive is one command line (sendfile on;) or block (server { … }) in nginx.conf. In the source it is one element of the ngx_command_t array a module exposes: a name, bit flags for where it may appear and how many arguments it takes, a function that handles the value (set), and where to put the value (conf, offset).

A config context is where a directive sits — main (top level of the file), events {}, http {}, server {}, location {} and so on. An HTTP module can keep a separate config struct per context; the three kinds are called main_conf, srv_conf and loc_conf.

index and ctx_index: every module has two numbers. index is its number among all modules; ctx_index is its number among modules of the same type. The first is the slot number in the cycle-wide config array; the second is the slot number in HTTP's main_conf[], srv_conf[] and loc_conf[] arrays.

Merging is what lets a lower context inherit a value written in a higher one. Write sendfile on; in http {} and sendfile is on in every location {} below it — thanks to merging. Each module implements it itself in callbacks such as merge_loc_conf().

A dynamic module is not built into the nginx executable; it is built as a separate .so file and read with load_module in the config file. The example module in this post is one.

cycle and phase were explained in earlier parts. The cycle (ngx_cycle_t) is the struct that holds the state of a running nginx — the config it read, the module list, the connection array — and a new one is built on every reload (part 2). A phase is one of the steps nginx splits the handling of an HTTP request into (access checks, producing content, …); modules join request handling by registering a function for a phase (part 9).

1. A module is one struct — ngx_module_t

struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;

    char                 *name;

    ngx_uint_t            spare0;
    ngx_uint_t            spare1;

    ngx_uint_t            version;
    const char           *signature;

    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    ngx_int_t           (*init_master)(ngx_log_t *log);

    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);

    void                (*exit_master)(ngx_cycle_t *cycle);

    /* ... spare_hook0 ~ spare_hook7 ... */
};

src/core/ngx_module.h lines 223–258

The fields fall into three groups.

  1. Numbers and labels — ctx_index, index, name, plus version and signature, which are checked when a dynamic module is loaded (section 2).
  2. What the module does — type, ctx (the type-specific callback struct) and commands (the directive table). These are the stars of this post.
  3. Process lifetime callbacks — init_module (in the master, after the config is read), init_process (when a worker starts), exit_process and friends. The master and workers come in part 6.

The key point is that ctx is a void *. Different module types need different callbacks, so ngx_module_t only says "this points at something"; the code that knows the type (ngx_http.c for HTTP modules) looks at type and reinterprets it as ngx_http_module_t *. An HTTP module's context looks like this:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

src/http/ngx_http_config.h lines 24–36

create_* means "make me an empty config struct and return it", merge_* means "pass the parent's (prev) values down to the child (conf)", and init_main_conf means "the http block has been read; fill the blanks in the main config with defaults". preconfiguration and postconfiguration are each called once, right before and right after the http block is read. When these eight are called is the subject of the diagram in section 5.

The structure of ngx_module_t. The ngx_module_t variable ngx_http_greet_module has the fields index, ctx_index, name, version, signature and type. ctx points to ngx_http_module_t with 8 callbacks, in four pairs: preconfiguration·postconfiguration, create_main_conf·init_main_conf, create_srv_conf·merge_srv_conf and create_loc_conf·merge_loc_conf. commands points to an array of ngx_command_t (the directive table); one element has a name, type bits, a set function, a conf offset and an offset. There are also process lifetime callbacks such as init_master, init_module and init_process. One module = one struct. Blue boxes are what this post follows; the grey box holds the process lifetime callbacks covered in part 6.

The smallest real module: empty_gif

One of the tiniest modules shipped with nginx is ngx_http_empty_gif_module (used as location = /pixel.gif { empty_gif; }, it returns a 1×1 transparent GIF). It has one directive, and all eight callback slots in its context are NULL. Its directive handler:

static char *
ngx_http_empty_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_empty_gif_handler;

    return NGX_CONF_OK;
}

src/http/modules/ngx_http_empty_gif_module.c lines 131–140

It has no config struct of its own, so it takes someone else's — the core module's loc_conf of the location {} being read — and plugs its own function into the handler function pointer. Requests that land in this location get their response from that function in the content phase. proxy_pass (ngx_http_proxy_pass()) and fastcgi_pass install their content handlers the same way. Where handler is called at request time is shown with the content phase in part 9.

The module variable itself (ngx_module_t ngx_http_empty_gif_module = { NGX_MODULE_V1, &ngx_http_empty_gif_module_ctx, ngx_http_empty_gif_commands, NGX_HTTP_MODULE, NULL, … }) is at lines 93–106 of the same file — it fills in the context, the directive table and the type, and leaves the seven process lifetime callbacks NULL.

Easy to trip on: NGX_MODULE_V1 fills seven fields at once

The first line of the initializer, NGX_MODULE_V1, is a macro:

#define NGX_MODULE_V1                                                         \
    NGX_MODULE_UNSET_INDEX, NGX_MODULE_UNSET_INDEX,                           \
    NULL, 0, 0, nginx_version, NGX_MODULE_SIGNATURE

#define NGX_MODULE_V1_PADDING  0, 0, 0, 0, 0, 0, 0, 0

src/core/ngx_module.h lines 216–220

It sets ctx_index and index to "not yet" ((ngx_uint_t) -1), name to NULL, spare0 and spare1 to 0, and version and signature to the nginx version and the build-option string at the time this module was compiled. That is why module sources never write their own numbers or names — nginx assigns the numbers while starting up (section 2). The trailing NGX_MODULE_V1_PADDING zeroes the eight spare_hook0~7 slots.

2. Where the module list comes from

The array configure generates: objs/ngx_modules.c

Which modules are in nginx is decided at build time. When you run ./configure --with-http_ssl_module …, the shell script auto/modules collects the enabled module names in order and writes a C file, objs/ngx_modules.c:

echo                                          >> $NGX_MODULES_C
echo 'ngx_module_t *ngx_modules[] = {'        >> $NGX_MODULES_C

for mod in $modules
do
    echo "    &$mod,"                         >> $NGX_MODULES_C
done

auto/modules lines 1601–1607

$modules is the list assembled on lines 1490–1572 of the same file in the order core → event → (thread pool) → HTTP → mail → stream → misc. The result is the array ngx_modules[] of module variable addresses and the array ngx_module_names[] of their names. The order of this array is the order of the modules. Directive lookup (section 4) and callback calls both walk it in this order. How this decides the order of filter modules is covered in part 10.

At startup: numbering

Right before calling ngx_init_cycle(), main() walks this array once:

ngx_int_t
ngx_preinit_modules(void)
{
    ngx_uint_t  i;

    for (i = 0; ngx_modules[i]; i++) {
        ngx_modules[i]->index = i;
        ngx_modules[i]->name = ngx_module_names[i];
    }

    ngx_modules_n = i;
    ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;

    return NGX_OK;
}

src/core/ngx_module.c lines 25–39

A static module's index is simply its position in the array. ngx_max_module is the number of static modules plus 128 slots for dynamic modules (NGX_MAX_DYNAMIC_MODULES, line 13), and the cycle's array of config pointers, conf_ctx, is created with that size (ngx_cycle.c line 200). Room is reserved up front so the array never has to be rebuilt when dynamic modules join later.

Next, ngx_cycle_modules() (ngx_module.c lines 42–62) inside ngx_init_cycle() copies ngx_modules[] into cycle->modules, separately for each cycle. The global array is not used directly because of dynamic modules: when a reload builds a new cycle, load_module lines may have been added or removed, so the module list can differ (why a new cycle is built is in part 4).

load_module: inserting a dynamic module

load_module is a directive of the core module ngx_core_module, handled by ngx_load_module():

    if (cf->cycle->modules_used) {
        return "is specified too late";
    }

    /* ... */

    handle = ngx_dlopen(file.data);

    /* ... */

    modules = ngx_dlsym(handle, "ngx_modules");

    /* ... */

    order = ngx_dlsym(handle, "ngx_module_order");

    for (i = 0; modules[i]; i++) {
        module = modules[i];
        module->name = names[i];

        if (ngx_add_module(cf, &file, module, order) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_CORE, cf->log, 0, "module: %s i:%ui",
                       module->name, module->index);
    }

src/core/nginx.c lines 1663–1719

It opens the .so with dlopen() and uses dlsym() to find an array with the very same name, ngx_modules inside it. That works because configure also generates a small ngx_modules.c for each dynamic module's .so (we'll see it in example 1). Each module in it is checked by ngx_add_module() and inserted into cycle->modules:

    if (module->version != nginx_version) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "module \"%V\" version %ui instead of %ui",
                           file, module->version, (ngx_uint_t) nginx_version);
        return NGX_ERROR;
    }

    if (ngx_strcmp(module->signature, NGX_MODULE_SIGNATURE) != 0) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "module \"%V\" is not binary compatible",
                           file);
        return NGX_ERROR;
    }

src/core/ngx_module.c lines 170–182

It compares the version and signature that NGX_MODULE_V1 baked into the module with those of the running nginx. The signature (NGX_MODULE_SIGNATURE, ngx_module.h lines 201–213) is three numbers such as pointer size followed by 34 0/1 characters. Most of the 34 are build conditions — "is epoll available", "are threads enabled", "is SSL enabled" — and 7 are fixed constants. These conditions change the shape of nginx's structs (which fields exist), so a mismatch would mean reading the same struct differently. That is why a dynamic module must be built for the same nginx version with the same configure options, and why the example reuses the configure arguments printed by nginx -V.

Once it passes, the module receives the smallest unused number as its index (ngx_module_index(), lines 279–315) and is placed in front of a module named in order — order matters for filter modules, so configure also generates ngx_module_order[] — or, with no order, appended at the end (lines 211–252).

ctx_index is assigned when the http block is reached

Unlike index, ctx_index is not assigned early in startup. When http { is reached and ngx_http_block() starts, ngx_count_modules(cf->cycle, NGX_HTTP_MODULE) counts only the HTTP modules and numbers them 0, 1, 2, …:

    for (i = 0; cycle->modules[i]; i++) {
        module = cycle->modules[i];

        if (module->type != type) {
            continue;
        }

        /* ... a module that already has a ctx_index (recounting after a reload) keeps it ... */

        /* search for some free index */

        module->ctx_index = ngx_module_ctx_index(cycle, type, next);

        if (module->ctx_index > max) {
            max = module->ctx_index;
        }

        next = module->ctx_index + 1;
    }

    /* ... the old cycle's module numbers are also taken into max ... */

    /* prevent loading of additional modules */

    cycle->modules_used = 1;

    return max + 1;

src/core/ngx_module.c lines 93–152

The return value max + 1 becomes ngx_http_max_module, the number of slots in each HTTP config array. At the end, cycle->modules_used = 1 closes the door. That flag is exactly what the first check in ngx_load_module() looked at. This is why load_module must come before blocks such as http {} and events {}: once a block starts being read, the modules have been counted and the array sizes fixed, so a module added afterwards wouldn't fit. Event modules are counted at events { (ngx_event.c line 971); stream and mail call the same function in their own blocks.

The order in which the module list is built. auto/configure collects static module names through auto/modules and generates ngx_modules[] and ngx_module_names[] in objs/ngx_modules.c (89 entries in this build). ngx_preinit_modules in main fills index with the array position and name with the name, and sets ngx_max_module to 89 plus 128. ngx_cycle_modules in ngx_init_cycle copies the static modules into cycle->modules. load_module is read by ngx_load_module with dlopen and dlsym, and ngx_add_module checks the version and signature and inserts the module into cycle->modules. When the http block is reached, ngx_http_block assigns ctx_index with ngx_count_modules and sets modules_used to 1. Any load_module after that fails with is specified too late. The yellow box is a file produced by the build, blue boxes are functions that run during startup, and the red box is what happens when the order is broken.

Example 1: building a dynamic module and looking at the module list

For this post I wrote a small module called ngx_http_greet_module — a port to nginx of the mod_greet module from httpd part 7. It has four directives:

Directive Arguments Allowed in Stored in Handler
greet none location (replaces core's clcf->handler) hand-written ngx_http_greet()
greet_message 1 http, server, location message in loc_conf ngx_conf_set_str_slot
greet_shout on/off http, server, location shout in loc_conf ngx_conf_set_flag_slot
greet_tag 1 http, server tag in srv_conf ngx_conf_set_str_slot

A request to a location with greet; prints the values that apply to that request and "which line of nginx.conf the config struct was created on". The create and merge callbacks log one line each time they are called. Here are the module description file (config) and the module source:

# Example written for this post (not part of the nginx source)
# module description file that configure reads via --add-dynamic-module=/work
ngx_addon_name=ngx_http_greet_module

ngx_module_type=HTTP
ngx_module_name=ngx_http_greet_module
ngx_module_srcs="$ngx_addon_dir/ngx_http_greet_module.c"

. auto/module
/* Example written for this post (not part of the nginx source) */
/* ... header comment describing the four directives ... */
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>


typedef struct {
    ngx_uint_t   line;      /* config file line being read when this struct was created */
    ngx_str_t    message;   /* greet_message — data == NULL means "not written" */
    ngx_flag_t   shout;     /* greet_shout   — NGX_CONF_UNSET means "not written" */
} ngx_http_greet_loc_conf_t;


/* ... ngx_http_greet_srv_conf_t (line, tag), function declarations ... */

static ngx_command_t  ngx_http_greet_commands[] = {

    { ngx_string("greet"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_greet,
      0,
      0,
      NULL },

    { ngx_string("greet_message"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_greet_loc_conf_t, message),
      NULL },

    { ngx_string("greet_shout"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_greet_loc_conf_t, shout),
      NULL },

    { ngx_string("greet_tag"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_SRV_CONF_OFFSET,
      offsetof(ngx_http_greet_srv_conf_t, tag),
      NULL },

      ngx_null_command
};


/* ... ngx_http_greet_module_ctx: only postconfiguration, create/merge_srv_conf, create/merge_loc_conf are set ... */
/* ... ngx_module_t ngx_http_greet_module = { NGX_MODULE_V1, ... } — same shape as empty_gif ... */
/* ... ngx_http_greet_handler(), ngx_http_greet() — installs the handler the same way as empty_gif ... */
/* ... postconfiguration, srv_conf create/merge — same shape as the loc_conf ones below ... */

static void *
ngx_http_greet_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_greet_loc_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_greet_loc_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    conf->line = cf->conf_file->line;
    conf->shout = NGX_CONF_UNSET;

    ngx_conf_log_error(NGX_LOG_NOTICE, cf, 0, "greet create_loc_conf");

    return conf;
}


static char *
ngx_http_greet_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_greet_loc_conf_t  *prev = parent;
    ngx_http_greet_loc_conf_t  *conf = child;

    ngx_conf_log_error(NGX_LOG_NOTICE, cf, 0,
                       "greet merge_loc_conf: line %ui -> line %ui, "
                       "message %s, shout %s",
                       prev->line, conf->line,
                       conf->message.data ? "set here" : "inherited",
                       conf->shout != NGX_CONF_UNSET ? "set here" : "inherited");

    ngx_conf_merge_str_value(conf->message, prev->message, "hello");
    ngx_conf_merge_value(conf->shout, prev->shout, 0);

    return NGX_CONF_OK;
}

ngx_conf_log_error() appends "the config file and line currently being read" (in /work/nginx.conf:12) to the message (ngx_conf_file.c lines 1025–1027), which is perfect for seeing when a callback ran.

The build runs inside the container with the installed nginx's configure arguments plus --add-dynamic-module, followed by make modules. The relevant part of run.sh and its output:

cd /usr/src/nginx
# same configure arguments as the installed nginx + --add-dynamic-module (load_module accepts it only if the signature matches)
eval ./auto/configure $(nginx -V 2>&1 | sed -n 's/^configure arguments: //p') \
    --add-dynamic-module=/work > /tmp/u05/configure.log 2>&1
make modules > /tmp/u05/make.log 2>&1
=== 1. build the dynamic module (configure + make modules) ===
72152 objs/ngx_http_greet_module.so
signature: 8,4,8,0010111111010111001111111111100110
=== 2. objs/ngx_http_greet_module_modules.c (generated by configure) ===

#include <ngx_config.h>
#include <ngx_core.h>

extern ngx_module_t  ngx_http_greet_module;

ngx_module_t *ngx_modules[] = {
    &ngx_http_greet_module,
    NULL
};

char *ngx_module_names[] = {
    "ngx_http_greet_module",
    NULL
};

char *ngx_module_order[] = {
    NULL
};

=== 3. /opt/nginx/ngx_modules.c (static modules of this build) ===
extern lines: 89
entries in ngx_modules[]: 89
     1	    &ngx_core_module,
     2	    &ngx_errlog_module,
     3	    &ngx_conf_module,
     4	    &ngx_openssl_module,
     5	    &ngx_openssl_cache_module,
     6	    &ngx_regex_module,
     7	    &ngx_events_module,
     8	    &ngx_event_core_module,
    11	    &ngx_http_module,
    12	    &ngx_http_core_module,
    16	    &ngx_http_static_module,
    34	    &ngx_http_proxy_module,
    69	    &ngx_stream_module,
    89	    &ngx_stream_upstream_zone_module,

I ran this example in a Docker container (Debian bookworm, --with-debug) with nginx 1.31.9 built unmodified from the reference commit (9a7439fd79). The .so size may differ with the build environment.

  1. The signature string is inside the .so. 8,4,8, are the sizes of a pointer, sig_atomic_t and time_t; the 34 digits after them are the build conditions (7 of them fixed constants). Built with the same configure arguments as the installed nginx, it passes the strcmp in ngx_add_module().
  2. A dynamic module's .so has its own ngx_modules[], ngx_module_names[] and ngx_module_order[]. This is the objs/<module>_modules.c file that auto/make lines 493–564 write for each module. It's a plain HTTP module, not a filter, so ngx_module_order[] is empty and the module goes at the end.
  3. This build has 89 static modules. Core and event modules come first, then ngx_http_module at #11 (a core-type module — it owns just the http {} block directive), HTTP modules from ngx_http_core_module at #12, and stream modules from #69.

Easy to trip on: ngx_http_module and ngx_http_core_module are different modules

The names are close, but ngx_http_module has type NGX_CORE_MODULE and a single directive, http (ngx_http.c lines 86–119). It has to be a core module to recognize http { at the top level (main context). ngx_http_core_module has type NGX_HTTP_MODULE and owns HTTP's basic directives such as server, location, listen, root and sendfile. events is split the same way into ngx_events_module (core type, the events block) and ngx_event_core_module (event type, worker_connections and so on). One module opens the outer block and another fills it in.

3. The directive table — ngx_command_t

struct ngx_command_s {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};

#define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }

src/core/ngx_conf_file.h lines 77–86

Field Meaning Value for greet_message
name directive name (ngx_str_t — length plus pointer) "greet_message"
type allowed-context bits + argument-count bits NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1
set function that handles the value ngx_conf_set_str_slot
conf which array (main/srv/loc) of the context struct to use — a byte offset NGX_HTTP_LOC_CONF_OFFSET
offset which field of the module's config struct to write — a byte offset offsetof(ngx_http_greet_loc_conf_t, message)
post optional check function called after the value is stored NULL

The table ends with ngx_null_command (an element whose name length is 0); lookups walk until cmd->name.len is 0.

type — one bit, one meaning

/*
 *        AAAA  number of arguments
 *      FF      command flags
 *    TT        command type, i.e. HTTP "location" or "server" command
 */

#define NGX_CONF_NOARGS      0x00000001
#define NGX_CONF_TAKE1       0x00000002
#define NGX_CONF_TAKE2       0x00000004
/* ... TAKE3 ~ TAKE7, combinations such as TAKE12 ... */

#define NGX_CONF_ARGS_NUMBER 0x000000ff
#define NGX_CONF_BLOCK       0x00000100
#define NGX_CONF_FLAG        0x00000200
#define NGX_CONF_ANY         0x00000400
#define NGX_CONF_1MORE       0x00000800
#define NGX_CONF_2MORE       0x00001000

#define NGX_DIRECT_CONF      0x00010000

#define NGX_MAIN_CONF        0x01000000
#define NGX_ANY_CONF         0xFF000000

src/core/ngx_conf_file.h lines 16–52

One 32-bit integer is split into three parts. The lowest two hex digits are the argument count — TAKE1 means "exactly 1", and ORing bits collects the allowed counts, e.g. TAKE12 for "1 or 2". Above that is the shape — BLOCK (a block opened with {), FLAG (on/off), 1MORE (one or more). The top two hex digits are the context. HTTP's context bits are in ngx_http_config.h:

#define NGX_HTTP_MODULE           0x50545448   /* "HTTP" */

#define NGX_HTTP_MAIN_CONF        0x02000000
#define NGX_HTTP_SRV_CONF         0x04000000
#define NGX_HTTP_LOC_CONF         0x08000000
#define NGX_HTTP_UPS_CONF         0x10000000
#define NGX_HTTP_SIF_CONF         0x20000000
#define NGX_HTTP_LIF_CONF         0x40000000
#define NGX_HTTP_LMT_CONF         0x80000000


#define NGX_HTTP_MAIN_CONF_OFFSET  offsetof(ngx_http_conf_ctx_t, main_conf)
#define NGX_HTTP_SRV_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, srv_conf)
#define NGX_HTTP_LOC_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, loc_conf)

src/http/ngx_http_config.h lines 39–52

UPS is inside upstream {}, SIF and LIF are inside an if () {} in server or location, and LMT is inside limit_except {}. sendfile, for example, is MAIN|SRV|LOC|LIF|FLAG, so it is also allowed in an if block inside a location (ngx_http_core_module.c lines 404–410). The "Context: http, server, location, if in location" line in the nginx docs is these bits translated into words.

The type of ngx_command_t is one 32-bit integer split into parts of two hex digits. Bits 24–31 are the context: MAIN_CONF 0x01, and for HTTP MAIN 0x02, SRV 0x04, LOC 0x08, UPS 0x10, SIF 0x20, LIF 0x40, LMT 0x80. Bits 16–23 are flags: DIRECT_CONF 0x01. Bits 8–15 are the shape: BLOCK 0x01, FLAG 0x02, ANY 0x04, 1MORE 0x08, 2MORE 0x10. Bits 0–7 are the argument count: NOARGS 0x01, TAKE1 0x02, TAKE2 0x04, up to TAKE7. For example, greet_message is MAIN·SRV·LOC·TAKE1, which is 0x0E000002, and sendfile is MAIN·SRV·LOC·LIF·FLAG, which is 0x4E000200. One type field holds the context, the shape and the argument count together. The values are the #defines in ngx_conf_file.h lines 16–52 and ngx_http_config.h lines 39–52.

Easy to trip on: 0x02000000 means three things

NGX_HTTP_MAIN_CONF (0x02000000) has the same value as NGX_EVENT_CONF (ngx_event.h line 397), NGX_STREAM_MAIN_CONF (ngx_stream.h line 317) and NGX_MAIL_MAIN_CONF (ngx_mail.h line 357). The top eight bits are reused by each type. So the context bits alone can't tell "inside http" from "inside events", and ngx_conf_handler() in section 4 compares the module type before checking the bits. The first error in example 3 is exactly this case.

Standard setters — most directives don't write a new function

A module may write its own set function, but common shapes — "one string", "on/off", "a number", "a size (8k)", "a time (60s)" — use functions core already provides, the slot functions (12 of them on lines 284–296 of ngx_conf_file.h). The one for on/off:

char *
ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    char  *p = conf;

    ngx_str_t        *value;
    ngx_flag_t       *fp;
    ngx_conf_post_t  *post;

    fp = (ngx_flag_t *) (p + cmd->offset);

    if (*fp != NGX_CONF_UNSET) {
        return "is duplicate";
    }

    value = cf->args->elts;

    if (ngx_strcasecmp(value[1].data, (u_char *) "on") == 0) {
        *fp = 1;

    } else if (ngx_strcasecmp(value[1].data, (u_char *) "off") == 0) {
        *fp = 0;

    } else {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                     "invalid value \"%s\" in \"%s\" directive, "
                     "it must be \"on\" or \"off\"",
                     value[1].data, cmd->name.data);
        return NGX_CONF_ERROR;
    }

    if (cmd->post) {
        post = cmd->post;
        return post->post_handler(cf, post, fp);
    }

    return NGX_CONF_OK;
}

src/core/ngx_conf_file.c lines 1031–1068

This function has no idea which module or which struct it is writing to. It takes the struct's start address (conf), adds cmd->offset bytes, treats the result as an ngx_flag_t and writes the value. The struct's layout reaches it only as a single number stored with offsetof() in the offset field of ngx_command_t. That's how sendfile, tcp_nopush, gzip and the example's greet_shout all share this one function.

Also note the line that returns "is duplicate" when *fp != NGX_CONF_UNSET. The value must still be "not set" (-1) to be written. So create_loc_conf() must initialize flag fields to NGX_CONF_UNSET — the example's conf->shout = NGX_CONF_UNSET; does that. Leave it at 0 and you can't tell "explicitly off" from "not written", and the merge in section 6 breaks too. For strings, data == NULL means "not written", so the zero from ngx_pcalloc() is enough (ngx_conf_set_str_slot, the if (field->data) on lines 1071–1095).

When a set function returns a string, that string becomes the tail of the error message: as section 4 shows, ngx_conf_handler() prefixes it with the directive name using "\"%s\" directive %s".

4. Finding a function by name — ngx_conf_handler()

ngx_conf_parse(), which we saw in part 4, collects the words of one line into the cf->args array with ngx_conf_read_token(), and calls ngx_conf_handler() when it hits ; or { (ngx_conf_file.c line 325). The name lookup:

    for (i = 0; cf->cycle->modules[i]; i++) {

        cmd = cf->cycle->modules[i]->commands;
        if (cmd == NULL) {
            continue;
        }

        for ( /* void */ ; cmd->name.len; cmd++) {

            if (name->len != cmd->name.len) {
                continue;
            }

            if (ngx_strcmp(name->data, cmd->name.data) != 0) {
                continue;
            }

            found = 1;

            if (cf->cycle->modules[i]->type != NGX_CONF_MODULE
                && cf->cycle->modules[i]->type != cf->module_type)
            {
                continue;
            }

            /* is the directive's location right ? */

            if (!(cmd->type & cf->cmd_type)) {
                continue;
            }

src/core/ngx_conf_file.c lines 374–403

No hash, no tree: it compares against every directive of every module in turn. Comparing lengths first filters out most of them. This lookup only runs while reading the config at startup and reload; it plays no part in request processing (httpd keeps a hash of directive names instead — httpd part 7).

When the name matches, it records found = 1 and checks two things:

If either fails, continue — a different module's directive with the same name may exist, so the search goes on. If nothing fits by the end, found decides between two errors (lines 486–496): no such name at all gives unknown directive; the name exists but not for this place gives is not allowed here.

After that come the argument-count checks (lines 405–449, using the argument_number[] table to map "n arguments → the TAKEn bit"), and then the conf pointer to pass to set:

            /* set up the directive's configuration context */

            conf = NULL;

            if (cmd->type & NGX_DIRECT_CONF) {
                conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];

            } else if (cmd->type & NGX_MAIN_CONF) {
                conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);

            } else if (cf->ctx) {
                confp = *(void **) ((char *) cf->ctx + cmd->conf);

                if (confp) {
                    conf = confp[cf->cycle->modules[i]->ctx_index];
                }
            }

            rv = cmd->set(cf, cmd, conf);

src/core/ngx_conf_file.c lines 451–469

The flow of ngx_conf_handler. ngx_conf_read_token splits one line into words such as greet_message, hi. It walks cycle->modules in order looking for the name in commands. If no module has it: unknown directive. If found, it checks whether the module type equals cf->module_type (CONF modules always pass), and if so whether cmd->type AND cf->cmd_type has the current context's bit set. If either fails it moves to the next candidate, and if nothing fits: is not allowed here. If it passes, it checks the semicolon, the opening brace and the argument count; wrong: invalid number of arguments. If right, it finds the conf pointer in one of three ways — DIRECT, MAIN, or cf->ctx plus cmd->conf — and calls cmd->set. If set returns a string, that string becomes the error message. The three red boxes are the error messages you see most often from nginx -t. Example 3 triggers them one by one.

Three ways to find the conf pointer

cf->ctx is "the config pointers of the current context". At the top level it is cycle->conf_ctx (a void * array indexed by module index, ngx_cycle.c line 269); inside http { it is an ngx_http_conf_ctx_t.

  1. NGX_DIRECT_CONF — ordinary core module directives (worker_processes, pid, load_module …). ngx_init_cycle() called create_conf() for every core module before reading the config and stored the structs in conf_ctx[index] (ngx_cycle.c lines 233–248), so the struct is handed over directly.
  2. NGX_MAIN_CONF without DIRECT — block directives such as http, events and stream. Instead of a struct, it passes the address of the slot conf_ctx[index]. The slot is still empty; the block handler creates its own context struct and plugs it into the slot. *(ngx_http_conf_ctx_t **) conf = ctx; at the top of ngx_http_block() (ngx_http.c line 145) does exactly that.
  3. Everything else (inside HTTP and other blocks) — it treats cf->ctx as a char *, adds cmd->conf bytes to pick one of the main_conf/srv_conf/loc_conf fields, and takes slot ctx_index from that array. Since NGX_HTTP_LOC_CONF_OFFSET was offsetof(ngx_http_conf_ctx_t, loc_conf), the result is ctx->loc_conf[ctx_index].

The path to the place where greet_message's value is stored. cycle->conf_ctx has ngx_max_module slots of void pointers, and the slot number is the module's index. The http block has plugged its ctx into conf_ctx[ngx_http_module.index]. The current context's ngx_http_conf_ctx_t has the fields main_conf, srv_conf and loc_conf, and since cmd->conf is NGX_HTTP_LOC_CONF_OFFSET, the loc_conf field is picked. loc_conf[] has ngx_http_max_module slots, and the slot number is the module's ctx_index. loc_conf[57] is ngx_http_greet_loc_conf_t (line, message, shout), and value[1] is stored at &message, its start address plus cmd->offset. Two offsets (cmd->conf, cmd->offset) and two numbers (index, ctx_index). 57 is the ctx_index the greet module received in the example build (example 2).

ngx_conf_handler() knows nothing about HTTP. It never even names ngx_http_conf_ctx_t; it treats it as "a void ** array at some byte offset in the context struct". That's why stream and mail build their own context structs (ngx_stream_conf_ctx_t …) in the same shape and reuse this function unchanged.

Example 3: which line prints each error message?

The last part of the example's run.sh runs nginx -t on a small config, changing one spot each time. The skeleton (line 2 is events, line 7 is inside the location):

load_module /tmp/u05/ngx_http_greet_module.so;
events { (line 2 — what gets tried here) }
http {
    server {
        listen 127.0.0.1:8090;
        location / {
            (line 7 — what gets tried here)
        }
    }
}
(line 11 — what gets tried here)
=== 7. directive errors (nginx -t) ===
--- greet_message inside events {}
nginx: [emerg] "greet_message" directive is not allowed here in /tmp/u05/bad.conf:2
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_tag inside location {}
nginx: [emerg] "greet_tag" directive is not allowed here in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- typo in the name
nginx: [emerg] unknown directive "greet_mesage" in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_shout maybe
nginx: [emerg] invalid value "maybe" in "greet_shout" directive, it must be "on" or "off" in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message with two arguments
nginx: [emerg] invalid number of arguments in "greet_message" directive in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message twice
nginx: [emerg] "greet_message" directive is duplicate in /tmp/u05/bad.conf:7
nginx: configuration file /tmp/u05/bad.conf test failed
--- greet_message without ;
nginx: [emerg] unexpected "}" in /tmp/u05/bad.conf:8
nginx: configuration file /tmp/u05/bad.conf test failed
--- load_module after http {}
nginx: [emerg] "load_module" directive is specified too late in /tmp/u05/bad.conf:11
nginx: configuration file /tmp/u05/bad.conf test failed
What I put in Message Printed by
greet_message inside events {} is not allowed here the context bit is the same 0x02000000, but the module type (HTTP ≠ EVENT) fails → ngx_conf_file.c lines 393–397; message on lines 486–491
greet_tag inside a location is not allowed here no NGX_HTTP_LOC_CONF bit → lines 401–403
greet_mesage (typo) unknown directive no module has that name → lines 493–494
greet_shout maybe; invalid value … must be "on" or "off" printed by ngx_conf_set_flag_slot() itself → lines 1055–1059
greet_message a b; invalid number of arguments TAKE1 but two arguments → lines 445–448; message on lines 500–502
greet_message twice in one location is duplicate ngx_conf_set_str_slot() returns "is duplicate" and ngx_conf_handler() adds the name on lines 479–480
missing ; unexpected "}" never reaches a directive; caught while reading tokens → ngx_conf_read_token() lines 690–693 (part 4)
load_module after http {} is specified too late modules_used is already 1 → nginx.c lines 1663–1665

Some set functions return a short string (is duplicate), others print with ngx_conf_log_error() themselves and return NGX_CONF_ERROR (invalid value …). Either way the message ends with in file:line, because both go through the same ngx_conf_log_error().

Easy to trip on: forgetting load_module gives "unknown directive"

The difference between the second and third rows of the table is practical. If you use a dynamic module's directive but forget the load_module line, that module isn't in cycle->modules, found stays 0, and you get unknown directive — the same message as for a typo. is not allowed here, on the other hand, means "the module is loaded and the name is right, but the place is wrong".

5. The http block — config struct arrays per context

Now to the HTTP side. When http { is reached, ngx_http_block() is called through the second path in section 4. This one function owns HTTP configuration from start to finish.

The context struct: three pointer arrays

typedef struct {
    void        **main_conf;
    void        **srv_conf;
    void        **loc_conf;
} ngx_http_conf_ctx_t;

src/http/ngx_http_config.h lines 17–21

All three fields are arrays of void *. They have ngx_http_max_module slots, and slot k points to the config struct of the HTTP module whose ctx_index == k. Every module's struct has a different shape, so they are held as void * and the module that reads one casts it to its own type. The macros for that are ngx_http_conf_get_module_loc_conf(cf, module) (at config time) and ngx_http_get_module_loc_conf(r, module) (at request time) (ngx_http_config.h lines 55–66).

ngx_http_block() — create, read, merge, finish

    /* count the number of the http modules and set up their indices */

    ngx_http_max_module = ngx_count_modules(cf->cycle, NGX_HTTP_MODULE);


    /* the http main_conf context, it is the same in the all http contexts */

    ctx->main_conf = ngx_pcalloc(cf->pool,
                                 sizeof(void *) * ngx_http_max_module);
    /* ... */

    /*
     * the http null srv_conf context, it is used to merge
     * the server{}s' srv_conf's
     */

    ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    /* ... */

    ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    /* ... */

    for (m = 0; cf->cycle->modules[m]; m++) {
        if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }

        module = cf->cycle->modules[m]->ctx;
        mi = cf->cycle->modules[m]->ctx_index;

        if (module->create_main_conf) {
            ctx->main_conf[mi] = module->create_main_conf(cf);
            /* ... */
        }

        if (module->create_srv_conf) {
            ctx->srv_conf[mi] = module->create_srv_conf(cf);
            /* ... */
        }

        if (module->create_loc_conf) {
            ctx->loc_conf[mi] = module->create_loc_conf(cf);
            /* ... */
        }
    }

src/http/ngx_http.c lines 148–217

In the http context it calls all three — create_main_conf, create_srv_conf and create_loc_conf — for every module. There is only one main config per http, so that one is obvious; the comment explains why srv and loc configs are created at the http level too — "null srv_conf … used to merge the server{}s' srv_conf's". They hold values written directly in http {}, such as sendfile on;, and serve as the merge parent of the servers.

It continues like this:

    pcf = *cf;
    cf->ctx = ctx;

    for (m = 0; cf->cycle->modules[m]; m++) {
        /* ... for each HTTP module ... */

        if (module->preconfiguration) {
            if (module->preconfiguration(cf) != NGX_OK) {
                return NGX_CONF_ERROR;
            }
        }
    }

    /* parse inside the http{} block */

    cf->module_type = NGX_HTTP_MODULE;
    cf->cmd_type = NGX_HTTP_MAIN_CONF;
    rv = ngx_conf_parse(cf, NULL);

src/http/ngx_http.c lines 219–240

It changes three fields of cf — ctx, module_type and cmd_type — and calls ngx_conf_parse(cf, NULL) again. A NULL file name means "keep reading the current file up to this block's }" (part 4). These three are exactly the values the module-type and context-bit checks in section 4 rely on. The old values are saved in pcf and restored at the end with *cf = pcf; (line 326) — change on the way into a block, restore on the way out: a hand-rolled stack.

server and location blocks — shared arrays and new arrays

server { is a directive of ngx_http_core_module, handled by ngx_http_core_server(). It repeats the same work one level down — it creates a new ngx_http_conf_ctx_t, shares the http one's main_conf pointer (ctx->main_conf = http_ctx->main_conf;), creates new srv_conf and loc_conf arrays, and calls create_srv_conf and create_loc_conf for every HTTP module (src/http/ngx_http_core_module.c lines 3031–3077).

Then it reads the inside of the server block after setting cf->cmd_type = NGX_HTTP_SRV_CONF; (lines 3098–3104). For location {, ngx_http_core_location() goes one more level down: it shares main_conf and srv_conf and creates only a new loc_conf:

    pctx = cf->ctx;
    ctx->main_conf = pctx->main_conf;
    ctx->srv_conf = pctx->srv_conf;

    ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    if (ctx->loc_conf == NULL) {
        return NGX_CONF_ERROR;
    }

    for (i = 0; cf->cycle->modules[i]; i++) {
        if (cf->cycle->modules[i]->type != NGX_HTTP_MODULE) {
            continue;
        }

        module = cf->cycle->modules[i]->ctx;

        if (module->create_loc_conf) {
            ctx->loc_conf[cf->cycle->modules[i]->ctx_index] =
                                                   module->create_loc_conf(cf);
            if (ctx->loc_conf[cf->cycle->modules[i]->ctx_index] == NULL) {
                return NGX_CONF_ERROR;
            }
        }
    }

    clcf = ctx->loc_conf[ngx_http_core_module.ctx_index];
    clcf->loc_conf = ctx->loc_conf;

src/http/ngx_http_core_module.c lines 3179–3205

The last two lines matter. The core module's location config (ngx_http_core_loc_conf_t) keeps its location's entire loc_conf array in clcf->loc_conf. Later, when a request is assigned to this location, moving just this one pointer into the request switches every module's location config at once (end of section 6).

How the context structs of http, server A and location /loud/ point to arrays. All three context structs point to the single main_conf[]. The http context points to srv_conf[] (the merge parent of the servers) and loc_conf[] (http). The server A context points to its own srv_conf[] and loc_conf[]. The location /loud/ context also points to server A's srv_conf[], and only its loc_conf[] is its own. Blue boxes are the ngx_http_conf_ctx_t created once per block; yellow boxes are arrays with one slot per module. The deeper the block, the fewer new arrays it creates.

So nginx.conf gets as many arrays as it has blocks. The example config (below) has http 1 + server 2 + location 4, so there are 7 loc_conf arrays, 3 srv_conf arrays and 1 main_conf array, each with as many slots as there are HTTP modules (58 in this build, counting the dynamic module). A module with create_loc_conf creates one struct per location block. It costs memory per block, but as section 6 shows, nothing is left to compute at request time.

Where does greet_message go when written in a server block?

greet_message has the NGX_HTTP_SRV_CONF bit, so it may appear in a server block, but its conf field is NGX_HTTP_LOC_CONF_OFFSET. Following the third path in section 4, when this directive is met in a server block the value goes into the server context's loc_conf[] — the server-level loc_conf that ngx_http_core_server() created — not into srv_conf. Directives you can write "in server and in location", like root and sendfile, nearly all live in loc_conf this way, and the server-level loc_conf becomes the merge parent of that server's locations. Values that exist once per server, like greet_tag or core's server_name and listen, live in srv_conf.

Example 2: the order of the callbacks

Here is the example nginx.conf, with line numbers because they appear in the log:

 1  # Example written for this post (not part of the nginx source)
 2  load_module /tmp/u05/ngx_http_greet_module.so;
 3  worker_processes 1;
 4  pid /tmp/u05/nginx.pid;
 5  error_log /tmp/u05/error.log notice;
 6  events { worker_connections 64; }
 7
 8  http {
 9      access_log off;
10      greet_message "from http";
11
12      server {
13          listen 127.0.0.1:8080;
14          greet_tag "A";
15
16          location / {
17              greet;
18          }
19
20          location /loud/ {
21              greet_message "from location";
22              greet_shout on;
23              greet;
24
25              location /loud/inner/ {
26                  greet;
27              }
28          }
29      }
30
31      server {
32          listen 127.0.0.1:8081;
33          greet_message "from server 2";
34
35          location / {
36              greet;
37          }
38      }
39  }

I ran nginx -t -e stderr to test the config only and get the log on the screen (why -e stderr is needed: see "Easy to trip on" below).

=== 4. nginx -t -e stderr (config-time callbacks) ===
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:8
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:8
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:12
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:12
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:16
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:20
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:25
2026/11/07 02:57:37 [notice] 2866#2866: greet create_srv_conf in /work/nginx.conf:31
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:31
2026/11/07 02:57:37 [notice] 2866#2866: greet create_loc_conf in /work/nginx.conf:35
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_srv_conf: line 8 -> line 12, tag set here in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 8 -> line 12, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 12 -> line 16, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 12 -> line 20, message set here, shout set here in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 20 -> line 25, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_srv_conf: line 8 -> line 31, tag inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 8 -> line 31, message set here, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet merge_loc_conf: line 31 -> line 35, message inherited, shout inherited in /work/nginx.conf:39
2026/11/07 02:57:37 [notice] 2866#2866: greet postconfiguration: index=89 ctx_index=57 modules_n=90 http modules=58 ngx_http_max_module=58 in /work/nginx.conf:39
nginx: the configuration file /work/nginx.conf syntax is ok
nginx: configuration file /work/nginx.conf test is successful

The timestamp and pid (2866) differ on every run.

  1. Creation happens the moment a block opens. One srv and one loc at line 8 (http {), one srv and one loc at lines 12 and 31 (server {), one loc at lines 16, 20, 25 and 35 (location {) — exactly one per block. A location inside a location, like /loud/inner/ on line 25, gets one too.
  2. All merging happens at line 39, that is, after the closing brace of http {} has been read. ngx_conf_parse() reads the whole block and returns, and only then does ngx_http_block() start merging (section 6). So inside a merge function, everything written in the lower blocks is already known.
  3. postconfiguration comes after merging. The greet module's index is 89 — the first free number after static modules 0–88 — its ctx_index is 57, there are 58 HTTP modules (57 static + greet), and ngx_http_max_module is 58 too.

The order inside ngx_http_block. ngx_http_block calls every HTTP module's create_main·srv·loc_conf in the http context (line 8), calls preconfiguration, then reads the inside of the http block with ngx_conf_parse. A server block is handled by ngx_http_core_server, which calls create_srv_conf·create_loc_conf (line 12), and create_loc_conf is called for each location block (lines 16·20·25). Once everything up to the closing brace (line 39) is read, init_main_conf and ngx_http_merge_servers are called for each module; merging calls merge_srv_conf and then merge_loc_conf for the server, the locations and nested locations. Then come the location trees and ngx_http_init_phases, every module's postconfiguration (registering phase handlers and filters), and ngx_http_init_phase_handlers and ngx_http_optimize_servers. Line numbers are from the example nginx.conf. All eight HTTP module callbacks are called inside this picture, once each, while the config is read.

postconfiguration comes late for a reason. Many modules use it to register phase handlers (for example, ngx_http_access_module adds a function to the ACCESS phase array) or to hook themselves into the filter chain. That requires the phase arrays to exist (ngx_http_init_phases(), ngx_http.c line 294), so it runs after them. ngx_http_init_phase_handlers() (line 329), which flattens the registered handlers into a single array, runs later still. Phases are in part 9 and the filter chain in part 10.

Checking with gdb: nested blocks mean nested ngx_conf_parse() calls

The create_loc_conf for /loud/inner/ is the fifth call. The function lives in a dynamic module that isn't loaded yet when gdb starts, so I enabled set breakpoint pending on, stopped at the fifth call and printed the stack:

gdb -batch -q \
    -ex 'set breakpoint pending on' \
    -ex 'break ngx_http_greet_create_loc_conf' \
    -ex 'run' -ex 'continue 4' \
    -ex 'bt' \
    -ex 'frame 1' \
    -ex 'print ctx->main_conf == pctx->main_conf' \
    -ex 'print ctx->srv_conf == pctx->srv_conf' \
    -ex 'print ctx->loc_conf == pctx->loc_conf' \
    -ex 'print ngx_http_max_module' \
    -ex 'kill' \
    --args nginx -t -q -c /work/nginx.conf 2>&1 \
 grep -v '^\[Thread\|^Using host libthread\|^warning: Error disabling\|^Breakpoint 1 at\|^Make breakpoint pending\|^\[Inferior'
#0  ngx_http_greet_create_loc_conf (cf=0x7fffffffe7f0) at /work/ngx_http_greet_module.c:219
#1  0x00005555555f19c2 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3197
#2  0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#3  0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#4  0x00005555555f20f9 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3360
#5  0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#6  0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#7  0x00005555555f161a in ngx_http_core_server (cf=0x7fffffffe7f0, cmd=0x555555728200 <ngx_http_core_commands+224>, dummy=0x555555786ef0) at src/http/ngx_http_core_module.c:3102
#8  0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#9  0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x0) at src/core/ngx_conf_file.c:325
#10 0x00005555555e73ff in ngx_http_block (cf=0x7fffffffe7f0, cmd=0x5555557278a0 <ngx_http_commands>, conf=0x555555785350) at src/http/ngx_http.c:240
#11 0x00005555555a4359 in ngx_conf_handler (cf=0x7fffffffe7f0, last=1) at src/core/ngx_conf_file.c:469
#12 0x00005555555a3e89 in ngx_conf_parse (cf=0x7fffffffe7f0, filename=0x555555784338) at src/core/ngx_conf_file.c:325
#13 0x000055555559fb97 in ngx_init_cycle (old_cycle=0x7fffffffe930) at src/core/ngx_cycle.c:286
#14 0x000055555557ee55 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:300
#1  0x00005555555f19c2 in ngx_http_core_location (cf=0x7fffffffe7f0, cmd=0x555555728430 <ngx_http_core_commands+784>, dummy=0x555555799388) at src/http/ngx_http_core_module.c:3197
3197	                                                   module->create_loc_conf(cf);
$1 = 1
$2 = 1
$3 = 0
$4 = 58

Addresses may differ between runs. I trimmed the breakpoint notices at the top (Breakpoint 1, … and so on) and the Kill the program … line at the end.

  1. The three frames ngx_conf_parse → ngx_conf_handler → block handler repeat once per nesting level: the file (#12, with a filename) → http (#10) → server (#7) → location /loud/ (#4) → location /loud/inner/ (#1). The config parser descends through blocks by recursion, and each block handler adjusts cf on the way. Every frame's cf has the same address (0x7fffffffe7f0): one local variable, conf in ngx_init_cycle(), is shared all the way down, with fields changed and restored.
  2. In frame #1 (ngx_http_core_location), comparing the new context ctx with the parent context pctx shows main_conf and srv_conf are the same arrays (1) and only loc_conf is a different array (0) — just like the diagram above.

Easy to trip on: log lines written while the config is read don't go to the config's error_log

At first I ran only nginx -t -c /work/nginx.conf, and greet's notice lines showed up neither on screen nor in /tmp/u05/error.log. While the config is read, cf->log is the old cycle's log that ngx_init_cycle() received (ngx_cycle.c lines 67 and 272). On first start that is the default log opened by ngx_log_init() — logs/error.log as fixed at build time unless -e is given, at level NGX_LOG_NOTICE (around lines 324–334 of ngx_log.c). The error_log /tmp/u05/error.log in the config isn't open yet. So the notices went to /opt/nginx/logs/error.log. [emerg] errors do show on screen because WARN and more severe levels are also written to stderr, separately from the file (the condition around lines 198–199 of ngx_log.c). Pointing the default log at the screen with -e stderr gives the output above.

6. Merging — once at config time, only a pointer at request time

Where merging happens: ngx_http_merge_servers()

Merging starts after ngx_http_block() has read the whole block:

    cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
    cscfp = cmcf->servers.elts;

    for (m = 0; cf->cycle->modules[m]; m++) {
        if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }

        module = cf->cycle->modules[m]->ctx;
        mi = cf->cycle->modules[m]->ctx_index;

        /* init http{} main_conf's */

        if (module->init_main_conf) {
            rv = module->init_main_conf(cf, ctx->main_conf[mi]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }

        rv = ngx_http_merge_servers(cf, cmcf, module, mi);
        if (rv != NGX_CONF_OK) {
            goto failed;
        }
    }

src/http/ngx_http.c lines 251–275

The outer loop is over modules. It picks one module, merges that module's config across every server and location to the bottom, then moves to the next module. The server list cmcf->servers is the array ngx_http_core_server() appends to each time it meets a server block (ngx_http_core_module.c lines 3086–3093).

    cscfp = cmcf->servers.elts;
    ctx = (ngx_http_conf_ctx_t *) cf->ctx;
    saved = *ctx;
    rv = NGX_CONF_OK;

    for (s = 0; s < cmcf->servers.nelts; s++) {

        /* merge the server{}s' srv_conf's */

        ctx->srv_conf = cscfp[s]->ctx->srv_conf;

        if (module->merge_srv_conf) {
            rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index],
                                        cscfp[s]->ctx->srv_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }

        if (module->merge_loc_conf) {

            /* merge the server{}'s loc_conf */

            ctx->loc_conf = cscfp[s]->ctx->loc_conf;

            rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index],
                                        cscfp[s]->ctx->loc_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }

            /* merge the locations{}' loc_conf's */

            clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];

            rv = ngx_http_merge_locations(cf, clcf->locations,
                                          cscfp[s]->ctx->loc_conf,
                                          module, ctx_index);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
    }

failed:

    *ctx = saved;

    return rv;

src/http/ngx_http.c lines 573–621

Three steps per server:

  1. http-level srv_conf (parent) → this server's srv_conf (child): merge_srv_conf
  2. http-level loc_conf (parent) → this server-level loc_conf (child): merge_loc_conf
  3. this server-level loc_conf (parent) → the locations inside it: ngx_http_merge_locations() walks the location list calling merge_loc_conf, and calls itself again for locations nested inside a location (the recursion on lines 626–667).

The merge order in example 2's log — 8 -> 12 (srv), 8 -> 12 (loc), 12 -> 16, 12 -> 20, 20 -> 25, then the second server — is exactly this order. Because parents are merged first, by the time 20 -> 25 runs, the line-20 struct (/loud/) has already inherited everything from above. So merging one level at a time is enough to carry a top-level value all the way down.

The merge order and results for the example 2 config. http (line 8) doesn't set greet_tag in srv and sets message "from http" (line 10) in loc. ① merge_srv_conf and ② merge_loc_conf merge it into server A (line 12). Server A sets tag "A" in srv itself and inherits the message in loc. ③ location / (line 16) inherits and ends up with message "from http", shout off. ④ location /loud/ (line 20) has message "from location" and shout on, set right there. ⑤ location /loud/inner/ (line 25) inside it inherits from /loud/: message "from location", shout on. ⑥ merge_srv_conf and ⑦ merge_loc_conf merge into server 2 (line 31): the srv tag has nothing to inherit, so it gets the default -, and the loc message is "from server 2", set on line 33. ⑧ location / (line 35) inherits message "from server 2". The circled numbers are the order of the merge log. "Set here / inherited" in each box comes from the merge log, and the final values from the curl output below.

During merging, ctx->srv_conf and ctx->loc_conf are also switched to the server or location being merged (lines 582, 596 and 649). That way a merge function can use ngx_http_conf_get_module_*_conf(cf, …) to look at another module's config at the same level.

The shape of a merge function: "if not written, the parent's; if the parent didn't write it either, the default"

Most modules' merge functions are a handful of macro lines:

#define ngx_conf_merge_value(conf, prev, default)                            \
    if (conf == NGX_CONF_UNSET) {                                            \
        conf = (prev == NGX_CONF_UNSET) ? default : prev;                    \
    }

src/core/ngx_conf_file.h lines 209–212

This is why section 3 insisted that create_*_conf initialize fields to NGX_CONF_UNSET: you have to recognize "not written" to decide whether to take the parent's value. Strings use ngx_conf_merge_str_value, which tests data == NULL (lines 244–253), and there are matching macros for sizes, times, buffer counts and so on.

ngx_http_core_module's merge function, ngx_http_core_merge_loc_conf(), starts on line 3814 and runs to line 4103, close to 300 lines, mostly lines like this:

    ngx_conf_merge_value(conf->sendfile, prev->sendfile, 0);

src/http/ngx_http_core_module.c line 3956

The default is the third argument of this line. That's how to confirm a default from the source without looking it up in the docs. A few samples:

Directive Default in the merge line Location Value written in the source's conf/nginx.conf
sendfile 0 (off) ngx_http_core_module.c line 3956 sendfile on; (line 27)
keepalive_timeout 75000 ms (75 s) lines 3988–3989 keepalive_timeout 65; (line 31)
root "html" (path relative to the prefix) lines 3823–3837 root html; inside a location
sendfile_max_chunk 2 * 1024 * 1024 (2 MB) lines 3957–3958 not written

sendfile is off by default, yet "nginx uses sendfile" is common knowledge because the sample config file shipped with nginx says sendfile on;. Write a config from scratch and it runs with sendfile off. Likewise the 65 seconds of keepalive_timeout in the sample is a written value; delete it and you get 75 seconds. root uses hand-written code instead of a simple macro — alias and a path containing variables (root_lengths, root_values) must be inherited as a set, and if the parent has nothing either, "html" is turned into a full path relative to the prefix (lines 3823–3837).

Example 2 continued: the values each request got

I started nginx with the same config and sent a request to each location:

=== 5. curl: merged values per location ===
http://127.0.0.1:8080/               tag=A (srv_conf from line 12), message="from http" shout=off (loc_conf from line 16)
http://127.0.0.1:8080/loud/          TAG=A (SRV_CONF FROM LINE 12), MESSAGE="FROM LOCATION" SHOUT=ON (LOC_CONF FROM LINE 20)
http://127.0.0.1:8080/loud/inner/    TAG=A (SRV_CONF FROM LINE 12), MESSAGE="FROM LOCATION" SHOUT=ON (LOC_CONF FROM LINE 25)
http://127.0.0.1:8081/               tag=- (srv_conf from line 31), message="from server 2" shout=off (loc_conf from line 35)
  1. / (line 16) wrote nothing, yet got "from http" from line 10. It came down two levels: line 8 (http) → line 12 (server A level) → line 16.
  2. /loud/inner/ (line 25) wrote nothing either, yet got "from location" and shout on written in /loud/. A location inside a location merges with the outer location as its parent.
  3. / on 8081 got line 33 — the greet_message written in the server block. As explained above, that value sat in the server level's loc_conf and came down to the location on line 35. greet_tag is in neither http nor this server, so it has the default "-".

Where shout is on, the module prints the whole line in upper case (that's how I wrote the example module).

No merging at request time

Once nginx decides which location a request goes to, it just attaches that location's already merged array to the request. When the request struct is created it attaches the default server's arrays (ngx_http_request.c lines 601–603); after the location is found it switches to that location's:

        if (len == (size_t) node->len) {

            if (node->exact) {
                r->loc_conf = node->exact->loc_conf;
                return NGX_OK;

            } else {
                r->loc_conf = node->inclusive->loc_conf;
                return NGX_AGAIN;
            }
        }

src/http/ngx_http_core_module.c lines 1598–1608

node->exact->loc_conf is the array saved by clcf->loc_conf = ctx->loc_conf; in section 5. Changing one pointer switches every module's location config at once. After that a module reads its own values with ngx_http_get_module_loc_conf(r, module) — r->loc_conf[module.ctx_index] — as the example's ngx_http_greet_handler() does. The location lookup tree and this function itself are covered in detail in part 9.

How configuration is attached at request time. At config time there are loc_conf arrays that are already merged — the default server's loc_conf array, and, for example, the loc_conf array of location /loud/ (line 20) saved by clcf->loc_conf = ctx->loc_conf. When the request struct is built, r->loc_conf points to the default server's array (ngx_http_request.c lines 601–603). After the location is found, r->loc_conf is switched to node->exact->loc_conf or node->inclusive->loc_conf; only that one pointer changes. A module reads its value with r->loc_conf[module.ctx_index], and nothing is merged at request time. A request only switches a pointer to an already-merged array. /loud/ is the location from Example 2, used as an example.

Easy to trip on: httpd merges at request time too

httpd, as we saw in httpd part 7, merges server configs at startup but merges directory configs (<Directory>, <Location>, .htaccess) on every request with ap_merge_per_dir_configs(). Which sections apply depends on the request path, several can overlap, and .htaccess is read at request time. nginx picks exactly one location per request (they don't stack), and nesting comes only from the block structure of the config file, so every possible merge result can be built ahead of time at startup. The flip side is that nginx has no "config read at request time" like .htaccess. For the same reason, changing a directive value requires a reload that builds a new cycle and redoes all the merging from scratch (part 6).

Going deeper

Summary

Concept One-line summary
Module One ngx_module_t struct variable. type gives its kind, ctx points to type-specific callbacks, commands to the directive table
objs/ngx_modules.c The array of module addresses configure writes. Its order is the order of directive lookup and callback calls. 89 static modules in this build
index / ctx_index Number among all modules (cycle->conf_ctx slot) / number among modules of one type (main_conf[], srv_conf[], loc_conf[] slot)
load_module Inserts the ngx_modules[] of a dlopened .so with ngx_add_module(). Version and signature must match, and it must come before the blocks (modules_used)
ngx_command_t Name, context and argument bits (type), handler (set), array selector (conf), field selector (offset)
Slot functions ngx_conf_set_flag_slot and friends. Write to conf + offset without knowing the struct. "is duplicate" if the field isn't UNSET
ngx_conf_handler() Compares against every module's directives in turn. Module type → context bits → argument count → conf pointer → set
ngx_http_conf_ctx_t Three pointer arrays: main_conf, srv_conf, loc_conf. One per block. Lower blocks share the upper arrays and create only their own level
Callback order create when a block opens → after everything up to } is read, init_main_conf and merge → postconfiguration
Merging At config time, per module, one level at a time: server → location → nested location. The default is the third argument of ngx_conf_merge_*
At request time No merging. The found location's clcf->loc_conf array is just attached to r->loc_conf

What's next

With the config read, nginx becomes a master process and starts the workers. We'll see where init_process — passed over here as a "process lifetime callback" — is called, and how nginx -s reload redoes everything in this post up to the merge from scratch in a new cycle and then quietly retires the old workers. That's part 6: Master and Workers — Processes, Signals and Zero-Downtime Reload.

The httpd series' part 6: Modules and Hooks and part 7: The Config File and Directives are a different answer to the same problem. A module being a single struct, a directive table (command_rec ↔ ngx_command_t), and per-context config create and merge callbacks are strikingly similar. Two things differ. httpd modules join request processing by hanging functions on hooks, while nginx modules plug directly into phase arrays and the filter chain in postconfiguration (parts 9 and 10). And httpd merges directory configs at request time, while nginx merges everything ahead of time at startup.

More posts