namalzi

← All posts

Digging into nginx's Source 4: Startup and Reading the Config — From main() to ngx_init_cycle

nginx · Published

When you start nginx, main() reads the command line, and ngx_init_cycle() reads nginx.conf and builds one complete set of configuration called a cycle — only then does the master process start running. We follow how one parser function handles three cases (a file, a block, and the -g command-line string), how ngx_conf_read_token() splits text into words one character at a time, where the error messages come from, and how reload calls the same ngx_init_cycle() again and inherits listening sockets and shared memory from the old cycle — using the nginx 1.31.9 source and gdb and strace examples.

What this post covers

This is the fourth post in the "Digging into nginx's Source" series. In Part 2 we drew the overall map — master and worker processes, modules, the cycle and configuration contexts — and in Part 3 we looked at the memory pools (ngx_pool_t) and basic data structures nginx uses. This time we cover the span from the moment you type nginx until just before the master process starts its loop, and how nginx.conf is read along the way.

Four questions:

Item Details
Project nginx
Version nginx 1.31.9 (master, development)
Commit 9a7439fd79 (2026-10-23)
Main files src/core/nginx.c, src/core/ngx_cycle.c·ngx_cycle.h, src/core/ngx_conf_file.c·ngx_conf_file.h, src/core/ngx_connection.c

All line numbers in this post refer to that commit. How to get the same source and build the Docker image for the examples is in Part 1.

ngx_cycle.c is 1,484 lines, ngx_conf_file.c is 1,505 and nginx.c is 1,747. This post picks only the main line of startup and the skeleton of the config parser. How a directive puts a value into a module's config struct (set and offset in ngx_command_t, create and merge in the http {} block) belongs to Part 5, and how the master swaps workers when it gets a signal belongs to Part 6.

On the map from Part 2, this post zooms into the "Startup · cycle" box in area "A. Preparation".

Where this post sits on the series map. Area A, Preparation, holds Startup · cycle (Part 4, this post), Modules · directives (Part 5) and Memory pools · data structures (Part 3). Below it come area B, Processes and events (Master · workers Part 6, Event loop Part 7), and then area C, One HTTP request (Reading the request Part 8, Phases · location Part 9, Filters · static files Part 10, Upstream · proxy Part 11, Load balancing · keepalive · stream Part 12). Only the Startup · cycle box is blue. The blue box is this post. The rest are grey.

Terms to know first

A cycle (ngx_cycle_t) is a struct holding "one complete set of configuration and every resource opened for it". Each module's config struct, the list of listening sockets, open log files, shared memory and the connection array all hang off it. The global variable ngx_cycle points to the cycle currently in use. It is the struct we put in the middle of the map in Part 2.

Old cycle and new cycle: the function that reads the config, ngx_init_cycle(old_cycle), takes the current cycle and builds and returns one more, new cycle. At startup it is given an almost empty fake cycle (init_cycle) as the "old cycle"; on reload it is given the real cycle that was running.

A core module (NGX_CORE_MODULE) is a module whose directives are used at the outermost level of nginx.conf (the main context). ngx_core_module with worker_processes and pid, ngx_events_module with the events {} block, and ngx_http_module with the http {} block are core modules. A core module can have two callbacks: create_conf (make an empty config struct) and init_conf (fill in defaults for values you didn't set). We skimmed what a module looks like in Part 2 and go into detail in Part 5.

Context bits: every directive records "where it may be used" as bits. The main context is NGX_MAIN_CONF, inside events {} is NGX_EVENT_CONF, inside server {} is NGX_HTTP_SRV_CONF, and so on. The parser carries the bits for the place it is currently reading in cf->cmd_type and ANDs them with the directive's bits.

A token is a word you get by cutting the config file at spaces, ;, { and }. worker_processes 2; is read as the words worker_processes and 2 plus the terminator ;.

The signaller: nginx started with -s, as in nginx -s reload, is not a server but a short-lived process that "sends a signal to the running master and exits". The source marks it with ngx_process = NGX_PROCESS_SIGNALLER. One surprise of this post is that this process reads the config file too.

1. main() — before and after reading the config

main() is lines 200–407 of src/core/nginx.c, a little over 200 lines. In the middle is one call to ngx_init_cycle(); before it is "getting ready to read the config", after it is "choosing what to do with the config we read".

The flow of main(). ngx_get_options() stores command-line options in globals; if there is only -v or -V it prints the version and stops. Otherwise it builds an empty cycle called init_cycle on the stack, goes through ngx_os_init, ngx_add_inherited_sockets and ngx_preinit_modules, and calls ngx_init_cycle(&init_cycle). On failure it returns 1. On success it splits three ways: -t or -T prints test is successful and returns 0; -s calls ngx_signal_process to kill the pid in the pid file; otherwise it goes through ngx_init_signals, ngx_daemon and ngx_create_pidfile into ngx_master_process_cycle or ngx_single_process_cycle. Yellow boxes are where a cycle is built; grey boxes are paths that end without starting a server.

Before reading the config: options and an empty cycle

First, ngx_get_options() (lines 832–992) is a switch that looks at argv one character at a time. It only records values in global variables. -c goes to ngx_conf_file, -g to ngx_conf_params, -t sets ngx_test_config = 1, -s sets ngx_signal and ngx_process = NGX_PROCESS_SIGNALLER. Only four words may follow -s — stop, quit, reopen, reload — and anything else ends right here with invalid option (lines 950–960).

Next, main() prepares the bare minimum needed before reading the config file: a log (ngx_log_init() — it doesn't know nginx.conf's error_log yet, so it uses the default path fixed at build time or the file given with -e), and an empty cycle on the stack.

    ngx_memzero(&init_cycle, sizeof(ngx_cycle_t));
    init_cycle.log = log;
    ngx_cycle = &init_cycle;

    init_cycle.pool = ngx_create_pool(1024, log);
    if (init_cycle.pool == NULL) {
        return 1;
    }

src/core/nginx.c lines 253–260

init_cycle is a local variable of main(). It has only a log, a pool, and the paths (prefix, conf_file, conf_param …) filled in right after by ngx_process_options() (lines 1038–1138). conf_ctx, which would hold module configs, stays NULL. This "cycle whose conf_ctx is NULL" plays the old cycle at startup, and the source recognizes it with the macro ngx_is_init_cycle(cycle) — (cycle->conf_ctx == NULL) (ngx_cycle.h line 125).

Three more things run before ngx_init_cycle():

After reading the config: three paths

    cycle = ngx_init_cycle(&init_cycle);
    if (cycle == NULL) {
        if (ngx_test_config) {
            ngx_log_stderr(0, "configuration file %s test failed",
                           init_cycle.conf_file.data);
        }

        return 1;
    }

    if (ngx_test_config) {
        if (!ngx_quiet_mode) {
            ngx_log_stderr(0, "configuration file %s test is successful",
                           cycle->conf_file.data);
        }

        /* ... with -T, print the config files that were read ... */

        return 0;
    }

    if (ngx_signal) {
        return ngx_signal_process(cycle, ngx_signal);
    }

src/core/nginx.c lines 300–338

Why is the pid file written at the end of main() and not inside ngx_init_cycle()? A source comment explains: "we do not create the pid file in the first ngx_init_cycle() call because we need to write the demonized process pid" (ngx_cycle.c lines 330–333). After ngx_daemon() forks, the pid changes.

Easy to get wrong: -s reload reads all of nginx.conf too

In the code above, if (ngx_signal) comes after ngx_init_cycle(). The path of the pid file to signal is in nginx.conf's pid directive, so the config has to be read — and that reading is the whole config parse inside ngx_init_cycle(), all the way into the http {} block. So if the config has a syntax error, nginx -s reload prints the error and exits without sending the signal. We confirm this with the example in section 8. The signaller does, however, return after reading the config and running the core modules' init_conf (step ④ in section 3). It never touches log files, shared memory or sockets.

2. ngx_cycle_t — where one set of configuration lives

The struct that ngx_init_cycle() fills in, struct ngx_cycle_s, is at ngx_cycle.h lines 39–86. It has a little over thirty fields; the table lists only the ones this post uses.

Field What it holds What you saw in nginx.conf
conf_ctx an array of pointers, one per module number (index), to that module's config the values of all directives
pool the pool everything in this cycle was allocated from; discarding the cycle means destroying this one pool —
modules the module array this cycle uses (static modules + dynamic modules loaded with load_module) load_module
listening the array of listening sockets (ngx_listening_t) listen
open_files the list of files to open access_log, error_log
shared_memory the list of shared memory zones limit_req_zone, proxy_cache_path keys_zone=, upstream { zone }
old_cycle the old cycle this cycle was built from —
conf_file, conf_param the config file path, the string given with -g -c, -g

What ngx_cycle_t holds. conf_ctx points to one slot per module index holding the values of every directive; modules to the array of static modules and modules loaded with load_module; listening to the ngx_listening_t array that comes from listen; open_files to the list of files to open, such as access_log and error_log; shared_memory to the list of shared memory zones such as limit_req_zone, keys_zone= and zone; conf_file and conf_param to the config file path and the string given with -c and -g; old_cycle to the old cycle passed in when this one was built. pool is the cycle pool: everything above is allocated from it, so dropping the cycle means destroying just this one pool. The table above drawn as a structure. The second line of each blue box is what it ties to in nginx.conf or on the command line, and all of it is allocated from the yellow cycle pool.

The four stars on conf_ctx (void ****) look scary at first. The outermost level is "a pointer array with one slot per module", and what each slot contains is up to the module. ngx_core_module's slot holds an ngx_core_conf_t directly; the events slot holds a pointer leading to "the array of event modules' configs"; the http slot holds a pointer to an ngx_http_conf_ctx_t with three arrays (main/srv/loc). void **** is the type that fits the deepest case. We saw the shape in Part 2's figure, and which slot a directive goes to is covered in Part 5. For this post, just remember "one slot per module". To read a slot you use the macro ngx_get_conf(conf_ctx, module) — conf_ctx[module.index] (ngx_conf_file.h line 180).

3. ngx_init_cycle() — nine steps to a new cycle

ngx_init_cycle() is a single function, lines 39–954 of ngx_cycle.c — more than 900 lines. It's long, but the order is straight. I split it into nine steps.

The nine steps of ngx_init_cycle. ① new pool, new cycle (paths and sizes taken from old_cycle) ② ngx_cycle_modules() and the core modules' create_conf ③ ngx_conf_param() then ngx_conf_parse() — -g first, then nginx.conf ④ the core modules' init_conf fills in defaults; a process started with -s returns here ⑤ pid and lock files, paths, open the log files ⑥ shared memory zones, reusing an old zone if it matches ⑦ listening sockets — inherit old fds, then ngx_open_listening_sockets() ⑧ ngx_init_modules() calls init_module of every module ⑨ clean up the old cycle. An error at ③, ⑤, ⑥ or ⑦ undoes only what was newly made and returns NULL, leaving the old cycle untouched. Yellow boxes prepare resources (memory, files, sockets); blue boxes call module callbacks.

① A new pool, a new cycle

    log = old_cycle->log;

    pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
    /* ... */

    cycle = ngx_pcalloc(pool, sizeof(ngx_cycle_t));
    /* ... */

    cycle->pool = pool;
    cycle->log = log;
    cycle->old_cycle = old_cycle;

src/core/ngx_cycle.c lines 67–83

The new cycle is born in its own pool (NGX_CYCLE_POOL_SIZE, 16KB blocks by default — NGX_DEFAULT_POOL_SIZE at ngx_palloc.h line 22). As we saw in Part 3, a pool is a bundle of memory that you never free piece by piece; you destroy it all at once. So every config struct, string and array belonging to this cycle comes from this pool, and discarding the cycle later takes a single ngx_destroy_pool(cycle->pool).

Lines 85–194 copy the paths (conf_file, prefix, error_log, conf_param) from the old cycle into the new pool, and size the arrays and lists (paths, open_files, shared_memory, listening) to match the old cycle's counts. The old cycle's pool will disappear later, so borrowing its pointers as-is would be wrong.

② The module array and the core modules' create_conf

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

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

        if (module->create_conf) {
            rv = module->create_conf(cycle);
            if (rv == NULL) {
                ngx_destroy_pool(pool);
                return NULL;
            }
            cycle->conf_ctx[cycle->modules[i]->index] = rv;
        }
    }

src/core/ngx_cycle.c lines 233–248

Just before this, conf_ctx is created with ngx_max_module slots (line 200), and ngx_cycle_modules() copies the static module array into this cycle's modules (line 227). Then it calls create_conf for core modules only. HTTP and event module configs are not created here; the directive function of the http {} or events {} block creates them when the parser reaches that block (section 4).

What create_conf does is "make an empty config struct and mark every field as not set yet". Here is ngx_core_module's:

    ccf->daemon = NGX_CONF_UNSET;
    ccf->master = NGX_CONF_UNSET;
    ccf->timer_resolution = NGX_CONF_UNSET_MSEC;
    ccf->shutdown_timeout = NGX_CONF_UNSET_MSEC;

    ccf->worker_processes = NGX_CONF_UNSET;
    ccf->debug_points = NGX_CONF_UNSET;

src/core/nginx.c lines 1162–1168

NGX_CONF_UNSET is -1 (ngx_conf_file.h line 56). Since 0 can be a normal value meaning "off", "not written in the config file" has to be marked separately so nginx knows where to put defaults.

③ Reading the config: -g first, then the file

    conf.ctx = cycle->conf_ctx;
    conf.cycle = cycle;
    conf.pool = pool;
    conf.log = log;
    conf.module_type = NGX_CORE_MODULE;
    conf.cmd_type = NGX_MAIN_CONF;

    /* ... */

    if (ngx_conf_param(&conf) != NGX_CONF_OK) {
        /* ... destroy the new pool and return NULL ... */
    }

    if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
        /* ... destroy the new pool and return NULL ... */
    }

src/core/ngx_cycle.c lines 269–290

ngx_conf_t conf is the parser's current state. It carries which cycle is being filled, which pool to allocate from, and which context the current position is in (module_type = NGX_CORE_MODULE, cmd_type = NGX_MAIN_CONF — the outermost level of the file). Since conf.ctx points to the freshly made conf_ctx, directives end up filling its slots.

The order matters. The string given with -g (ngx_conf_param()) is read first, and nginx.conf (ngx_conf_parse()) after it. The trap this order sets is shown in section 6. What the parser looks like is the subject of sections 4 and 5.

④ The core modules' init_conf, and the signaller's exit

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

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

        if (module->init_conf) {
            /* ... call module->init_conf(); return NULL on NGX_CONF_ERROR ... */
        }
    }

    if (ngx_process == NGX_PROCESS_SIGNALLER) {
        return cycle;
    }

src/core/ngx_cycle.c lines 297–318

init_conf is called only after the whole file has been read. This is where fields still "not set" get their defaults. The first lines of ngx_core_module_init_conf() are exactly the defaults we know — daemon 1, master_process 1, worker_processes 1 (nginx.c lines 1191–1196). It's also this function that fills in the build-time path (NGX_PID_PATH) if you didn't write pid.

A signaller started with -s returns here. All it needs is the pid file path.

⑤ Files, ⑥ shared memory, ⑦ listening sockets

The rest are the steps that actually acquire the operating-system resources the config asks for.

⑥ and ⑦ have something in common: if the old cycle has the same thing, it is inherited instead of created anew. That is the secret behind reload not dropping connections, so section 7 looks at it separately.

It's also worth noticing that these resources are acquired all at once after parsing is finished. The listen 8080; directive doesn't open a socket when it's read. When the http {} block ends, ngx_http_optimize_servers() (ngx_http.c line 336) leads to ngx_create_listening(), which only adds an entry with fd = -1 to cycle->listening (ngx_connection.c lines 28 and 79). access_log and limit_req_zone likewise just put names on lists (shared memory via ngx_shared_memory_add(), lines 1306–1376). Operating-system resources are touched only after the whole config is known to be valid, so a config with a syntax error fails before a single log file, shared memory zone or listening socket is set up (the config file itself and any included files are, of course, opened while being read).

⑧ init_module, ⑨ cleaning up the old cycle

    if (ngx_open_listening_sockets(cycle) != NGX_OK) {
        goto failed;
    }

    if (!ngx_test_config) {
        ngx_configure_listening_sockets(cycle);
    }


    /* commit the new cycle configuration */

    if (!ngx_use_stderr) {
        (void) ngx_log_redirect_stderr(cycle);
    }

    pool->log = cycle->log;

    if (ngx_init_modules(cycle) != NGX_OK) {
        /* fatal */
        exit(1);
    }

src/core/ngx_cycle.c lines 632–652

The /* commit the new cycle configuration */ comment is the boundary. Before it, a failure can be rolled back; after it, the new cycle is final. ngx_init_modules() calls the init_module callback of every module (ngx_module.c lines 66–79). For example, the event core module grabs a small piece of shared memory used by accept_mutex and friends here (ngx_event_module_init(), from line 488 of src/event/ngx_event.c). A failure at this step isn't rolled back but ends in exit(1) — "fatal", just as the comment says.

Then lines 655–779 close the shared memory zones and listening sockets that exist only in the old cycle and not in the new one, and close all of the old cycle's files (the new cycle already reopened them in step ⑤). Finally it decides when to destroy the old cycle's pool.

    if (ngx_process == NGX_PROCESS_MASTER || ngx_is_init_cycle(old_cycle)) {

        ngx_destroy_pool(old_cycle->pool);
        cycle->old_cycle = NULL;

        return cycle;
    }

src/core/ngx_cycle.c lines 783–789

At startup (the old cycle is init_cycle) and in the master process, the old cycle's pool is destroyed right away. The master holds no client connections, so it has no reason to keep the old config. The ones handling connections with the old config are the old workers, and each worker got a full copy of the old cycle when it was forked, so it doesn't matter that the master destroys its own copy. In other cases (running a single process with master_process off), the old cycle is put in the ngx_old_cycles array and a 30-second timer (ngx_clean_old_cycles()) checks whether that cycle's connections are all gone before cleaning it up (lines 792–830).

The forks in the second half of ngx_init_cycle. If step ⑤ files, ⑥ shared memory or ⑦ listening sockets fails, goto failed closes only what the new cycle opened and destroys the new pool; the old cycle stays alive. On success it passes the /* commit the new cycle configuration */ comment and the new cycle is final. ⑧ ngx_init_modules() does not roll back on failure; it calls exit(1). ⑨ closes the zones and listening sockets that only the old cycle has, and closes all of the old cycle's files. Then, if this is the master process or the old cycle is the startup init_cycle, the old cycle's pool is destroyed right away with ngx_destroy_pool(), because workers use their own copy from fork. Otherwise (master_process off) the old cycle is pushed onto the ngx_old_cycles array, and a 30-second timer, ngx_clean_old_cycles(), clears it once its connections are done. The commit comment is the line: fail before it and nginx rolls back, fail after it and nginx exits. The source is ngx_cycle.c lines 632–830, with the failure path at 833–953.

On failure: close only what was newly opened

If it jumps to goto failed midway, lines 833–953 undo only what the new cycle newly opened. They close newly opened files, free new shared memory that isn't the same as the old cycle's, close newly opened sockets (ls[i].open), and destroy the new pool. The old cycle was never touched, so it stays alive. That structure is why the server keeps running with the old config when a reload fails (section 8).

Example 1: when, and by whom, is ngx_init_cycle() called?

I set a gdb breakpoint on ngx_init_cycle() that prints the process id, the value of ngx_process and the old cycle's conf_ctx on every entry, then continues. I started the master under gdb (-g 'daemon off;' — gdb can't follow it once it daemonizes), and meanwhile ran nginx -s reload under gdb from another shell as well.

This example was run with nginx 1.31.9 built unchanged from the reference commit (9a7439fd79), in a Docker container (Debian bookworm, --with-debug). The example folder is ex/u04-init-cycle-gdb. The gdb command file (init_cycle.gdb) attaches commands with one printf, bt 3 and continue to break ngx_init_cycle. Forked workers are let go (set detach-on-fork on), and gdb is told not to stop on signals like SIGHUP and SIGCHLD.

The gdb output on the master side (gdb's banner lines removed):

--- ngx_init_cycle() in pid 49, ngx_process=0, old_cycle->conf_ctx=(nil)
#0  ngx_init_cycle (old_cycle=0x7fffffffe930) at src/core/ngx_cycle.c:57
#1  0x000055555557ee55 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:300
--- ngx_init_cycle() in pid 49, ngx_process=1, old_cycle->conf_ctx=0x555555785310
#0  ngx_init_cycle (old_cycle=0x555555784120) at src/core/ngx_cycle.c:57
#1  0x00005555555ca301 in ngx_master_process_cycle (cycle=0x555555784120) at src/os/unix/ngx_process_cycle.c:247
#2  0x000055555557f1c0 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:403
--- ngx_init_cycle() in pid 49, ngx_process=1, old_cycle->conf_ctx=0x5555557885e0
#0  ngx_init_cycle (old_cycle=0x555555788130) at src/core/ngx_cycle.c:57
#1  0x00005555555ca301 in ngx_master_process_cycle (cycle=0x555555788130) at src/os/unix/ngx_process_cycle.c:247
#2  0x000055555557f1c0 in main (argc=5, argv=0x7fffffffed08) at src/core/nginx.c:403

And the output from the gdb that ran nginx -s reload:

--- ngx_init_cycle() in pid 76, ngx_process=2, old_cycle->conf_ctx=(nil)
#0  ngx_init_cycle (old_cycle=0x7fffffffe940) at src/core/ngx_cycle.c:57
#1  0x000055555557ee55 in main (argc=5, argv=0x7fffffffed18) at src/core/nginx.c:300

Pids and addresses differ on every run. The master's third call is the SIGHUP sent with a broken config in section 8's example.

  1. The first call comes from line 300 of main(), with old_cycle->conf_ctx=(nil) — the old cycle is init_cycle on the stack (the 0x7fff… address is in the stack region). At this point ngx_process is still 0 (NGX_PROCESS_SINGLE), because it only becomes master after the config is read, at lines 346–348 of main().
  2. The second call comes from line 247 of ngx_master_process_cycle() — that's reload. This time the old cycle is a real cycle at a heap address with conf_ctx filled in. One function, with a single shape — "take the old, build the new" — handles both startup and reload.
  3. The third call's old cycle (0x555555788130) is the cycle the second call built. Its address differs from the second call's old cycle (0x555555784120). Each reload moves the cycle forward one generation.
  4. The signaller (pid 76) calls ngx_init_cycle() too. With ngx_process=2 (NGX_PROCESS_SIGNALLER) it returns at step ④'s exit.

Example 1's output in time order. The master's (pid 49) call 1 happens at line 300 of main() with ngx_process=0; the old cycle is init_cycle on the stack (0x7fffffffe930, conf_ctx nil), and it builds the new cycle 0x555555784120. The signaller nginx -s reload (pid 76) also calls ngx_init_cycle from line 300 of main() with ngx_process=2; its old cycle is init_cycle on its own stack (0x7fffffffe940, conf_ctx nil), and it returns at the ④ exit and sends SIGHUP to the master. The master's call 2 then happens at line 247 of ngx_master_process_cycle() with ngx_process=1 and builds the new cycle 0x555555788130 from the old cycle 0x555555784120. When section 8 sends kill -HUP with a broken config, call 3 at line 247 takes the old cycle 0x555555788130, fails with unknown directive, and the old cycle keeps running. The gdb output above, laid out in time order. The pids and addresses are Example 1's actual output; the result of call 3 is from Example 5's output in section 8.

The values of ngx_process are the #defines at lines 23–27 of src/os/unix/ngx_process_cycle.h (0 SINGLE, 1 MASTER, 2 SIGNALLER, 3 WORKER, 4 HELPER).

4. ngx_conf_parse() — a file, a block and the command line in one function

ngx_conf_parse(cf, filename) is the single entry point for reading config (ngx_conf_file.c lines 161–358). But it is called in three different situations.

Case Called from filename Ends when
parse_file ngx_init_cycle(), include a file path the file ends (NGX_CONF_FILE_DONE)
parse_block the function of a block directive like events {}, http {}, server {} NULL (continue the file being read) the matching } (NGX_CONF_BLOCK_DONE)
parse_param ngx_conf_param() — -g NULL (just a string in a buffer) the string ends

The top of the function tells the three apart:

    if (filename) {

        /* open configuration file */

        fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);

        /* ... */

        buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
        /* ... */

        type = parse_file;

        /* ... */

    } else if (cf->conf_file->file.fd != NGX_INVALID_FILE) {

        type = parse_block;

    } else {
        type = parse_param;
    }

src/core/ngx_conf_file.c lines 179–245

With a file name, it opens the file and makes a 4KB buffer (NGX_CONF_BUFFER, line 11). Without one, whether there is "a file currently being read" separates a block from -g. For -g, ngx_conf_param() builds a fake file whose fd is NGX_INVALID_FILE, puts the string in its buffer and passes it in (lines 73–96).

Then comes an endless loop. Shortened, it looks like this:

    for ( ;; ) {
        rc = ngx_conf_read_token(cf);

        /* ... */

        if (rc == NGX_CONF_BLOCK_DONE) {

            if (type != parse_block) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "unexpected \"}\"");
                goto failed;
            }

            goto done;
        }

        /* ... end of file (NGX_CONF_FILE_DONE) and { inside -g are checked the same way ... */

        /* rc == NGX_OK || rc == NGX_CONF_BLOCK_START */

        /* ... if cf->handler is set (blocks like types {}), hand it over ... */

        rc = ngx_conf_handler(cf, rc);

        if (rc == NGX_ERROR) {
            goto failed;
        }
    }

src/core/ngx_conf_file.c lines 248–330

Once ngx_conf_read_token() has read one directive (section 5), ngx_conf_handler() finds the directive with that name and calls its function (section 6). Each terminator has a fixed reaction:

Getting inside a block — recursion

So when events { is reached, what reads the inside of the block? ngx_conf_parse() only finds the directive named events and calls its function, ngx_events_block(). What reads the inside is the ngx_conf_parse(cf, NULL) that this function calls in turn.

    pcf = *cf;
    cf->ctx = ctx;
    cf->module_type = NGX_EVENT_MODULE;
    cf->cmd_type = NGX_EVENT_CONF;

    rv = ngx_conf_parse(cf, NULL);

    *cf = pcf;

src/event/ngx_event.c lines 1001–1008

  1. Copy the whole current parser state *cf into the local variable pcf.
  2. Change the context — the place to fill (ctx) becomes the event modules' config array, the module type NGX_EVENT_MODULE, the context bits NGX_EVENT_CONF.
  3. ngx_conf_parse(cf, NULL) — no file name and a file currently open, so this is parse_block. It keeps reading the same buffer of the same file and returns at the matching }.
  4. Restore the original context with *cf = pcf. The next directive is read in the main context again.

The recursion that reads a block. ngx_init_cycle calls ngx_conf_parse with the file name, cmd_type=MAIN and module_type=CORE. When ngx_conf_read_token returns worker_processes 1 with NGX_OK, ngx_conf_handler finds the directive, calls set and returns. Next, when events comes with NGX_CONF_BLOCK_START, ngx_conf_handler calls ngx_events_block, which saves cf into pcf, switches ctx and type to EVENT, and calls ngx_conf_parse(NULL). This inner parser keeps reading the same file, handles worker_connections 64, gets NGX_CONF_BLOCK_DONE at the } and returns. ngx_events_block restores cf, calls init_conf and returns, and the outer parser continues in the main context. One block is one level of C function calls. http {} → server {} → location {} puts you three levels below the main context.

http {} has the same shape — ngx_http_block() switches to module_type = NGX_HTTP_MODULE and cmd_type = NGX_HTTP_MAIN_CONF at lines 238–240 and calls ngx_conf_parse(cf, NULL); server {} goes one level deeper with NGX_HTTP_SRV_CONF (ngx_http_core_module.c lines 3100–3104), and location {} with NGX_HTTP_LOC_CONF (lines 3358–3360). So the nesting of braces in nginx.conf becomes the depth of the C call stack. How the block functions create the modules' config structs inside, and merge them after the block, is in Part 5.

Compared with httpd the difference is clear. httpd first reads the whole config file into a tree (ap_directive_t) and then walks that tree to run the directives (httpd Part 7). nginx has no such tree. It calls the directive function as soon as the words are read, and enters blocks through function calls. There's no intermediate data structure, and directives run in the order they appear in the file.

Example 2: what the parser sees for each directive

I put a breakpoint on ngx_conf_handler() and, on every call, printed the current context bits (cf->cmd_type), the module type (the four bytes of cf->module_type as characters), the line number, and the word array (cf->args). I ran nginx -t -g 'daemon off;' under gdb. The example folder is ex/u04-conf-tokens and the config is:

# Example written for this post (not part of the nginx source)
# A config for seeing how ngx_conf_read_token() splits a line into words
worker_processes  1;   # a comment is dropped up to the end of the line
error_log /tmp/u04t/error.log;
pid /tmp/u04t/nginx.pid;

events { worker_connections 64; }

http {
    access_log off;
    log_format main '$remote_addr "$request"';
    server {
        listen 127.0.0.1:8080;
        location / {
            return 200 "a b; c \"quoted\"";
        }
        location /v { return 200 ${uri}x; }
    }
}
=== ngx_conf_handler() calls during nginx -t -g 'daemon off;' ===
cmd_type=01000000 module_type=CORE line= 0 | <daemon> <off>  last=0
cmd_type=01000000 module_type=CORE line= 3 | <worker_processes> <1>  last=0
cmd_type=01000000 module_type=CORE line= 4 | <error_log> </tmp/u04t/error.log>  last=0
cmd_type=01000000 module_type=CORE line= 5 | <pid> </tmp/u04t/nginx.pid>  last=0
cmd_type=01000000 module_type=CORE line= 7 | <events>  last=1
cmd_type=02000000 module_type=EVNT line= 7 | <worker_connections> <64>  last=0
cmd_type=01000000 module_type=CORE line= 9 | <http>  last=1
cmd_type=02000000 module_type=HTTP line=10 | <access_log> <off>  last=0
cmd_type=02000000 module_type=HTTP line=11 | <log_format> <main> <$remote_addr "$request">  last=0
cmd_type=02000000 module_type=HTTP line=12 | <server>  last=1
cmd_type=04000000 module_type=HTTP line=13 | <listen> <127.0.0.1:8080>  last=0
cmd_type=04000000 module_type=HTTP line=14 | <location> </>  last=1
cmd_type=08000000 module_type=HTTP line=15 | <return> <200> <a b; c "quoted">  last=0
cmd_type=04000000 module_type=HTTP line=17 | <location> </v>  last=1
cmd_type=08000000 module_type=HTTP line=17 | <return> <200> <${uri}x>  last=0
nginx: the configuration file /work/nginx.conf syntax is ok
nginx: configuration file /work/nginx.conf test is successful

last is the return value of ngx_conf_read_token() (0 = NGX_OK, ended by ; / 1 = NGX_CONF_BLOCK_START, ended by {).

  1. The first line is daemon off from -g, at line number 0 — it is read before the file (step ③ in section 3). ngx_conf_param() sets the fake file's line to 0 (line 91).
  2. Block directives (events, http, server, location) have last=1, and the context changes from the next line on — CORE/01000000 → EVNT/02000000, HTTP/02000000 → 04000000 → 08000000. That's the recursion from section 4 changing the context. After the location / block ended, location /v was read in 04000000 (the server context) again.
  3. module_type reads as letters. The module type constants are four characters packed into an integer — NGX_CORE_MODULE is 0x45524F43 /* "CORE" */ (ngx_conf_file.h line 70). On a little-endian machine, reading the bytes in memory order gives "CORE".
  4. Spaces and ; inside quotes don't split words, the quotes themselves are dropped, and \" becomes " — <a b; c "quoted">. The { in ${uri}x wasn't taken as a block start either. That's the subject of section 5.

Easy to get wrong: 0x02000000 means three things

In the output above, the inside of events {} and the level directly inside http {} both have cmd_type=02000000. The values really do overlap.

#define NGX_DIRECT_CONF      0x00010000

#define NGX_MAIN_CONF        0x01000000
#define NGX_ANY_CONF         0xFF000000

src/core/ngx_conf_file.h lines 49–52

NGX_EVENT_CONF is 0x02000000 (ngx_event.h line 397), NGX_HTTP_MAIN_CONF is also 0x02000000 (ngx_http_config.h line 41), and so are NGX_STREAM_MAIN_CONF and NGX_MAIL_MAIN_CONF. The top 8 bits (TT, the "command type" in the header comment at lines 16–20) are a space each module type uses on its own, and what tells them apart is cf->module_type. ngx_conf_handler() checks the module type before it looks at the context bits (section 6). So if you write worker_connections (an event module directive) directly inside http {}, the bits match but the module type doesn't, and it is rejected.

5. ngx_conf_read_token() — reading one character at a time into words

ngx_conf_read_token() (lines 509–823) is a hand-written state machine that looks at one character at a time, with no regular expressions or parser generator. Its state is a handful of flag variables.

Variable Meaning
last_space the previous character was a space — the next one starts a new word
need_space a quoted word just ended — the next must be a space, ; or {
d_quoted, s_quoted inside "…" / '…'
quoted the previous character was \ — this one has no special meaning
sharp_comment a # was seen — drop everything to the end of the line
variable the previous was $ — a { right after it is ${name}, not a block

The states of ngx_conf_read_token. It starts between words (last_space=1). Spaces and newlines keep it between words. A hash sign moves to comment (sharp_comment=1), which returns at the end of the line. An ordinary character, dollar or backslash moves inside a word. A double or single quote moves inside quotes. Inside a word, a space ends one word and goes back to between words. Inside quotes, the matching quote ends one word and moves to after a closing quote (need_space=1); a space there goes back to between words. A semicolon or opening brace inside a word or after a closing quote returns, and between words a semicolon, opening or closing brace also returns. Only the main flow is drawn. For the one character after \ and the ${ exception, see the text.

The decision made where a new word starts is the heart of the function:

            switch (ch) {

            case ';':
            case '{':
                if (cf->args->nelts == 0) {
                    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                       "unexpected \"%c\"", ch);
                    return NGX_ERROR;
                }

                if (ch == '{') {
                    return NGX_CONF_BLOCK_START;
                }

                return NGX_OK;

            case '}':
                if (cf->args->nelts != 0) {
                    ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                       "unexpected \"}\"");
                    return NGX_ERROR;
                }

                return NGX_CONF_BLOCK_DONE;

            case '#':
                sharp_comment = 1;
                continue;

src/core/ngx_conf_file.c lines 673–700

When a word ends (lines 766–820), it takes memory from the pool and copies the word, removing the \ from \", \' and \\ and turning \t, \r and \n into a real tab, CR and LF (lines 777–807). Then at line 808 it appends a '\0'. In Part 3 we said ngx_str_t strings may not end in NUL, but config words, at least, do end in NUL. That's why directive functions can use C string functions directly, as in ngx_strcmp(value[1].data, "auto") (ngx_set_worker_processes(), nginx.c line 1637). It's also why Example 2's gdb could print words with %s.

The 4KB buffer and "too long parameter"

The file isn't read all at once but a little at a time through the 4KB buffer. When the buffer is used up, lines 539–617 move the unfinished piece of the current word to the front of the buffer (ngx_memmove) and fill the rest from the file. But if a single word takes up the whole buffer (4,096 bytes), moving it frees no room, and that's an error (lines 563–583). If it was inside quotes, the message guesses "it seems you didn't close the quote" — too long parameter, probably missing terminating """ character. Forgetting a closing quote easily turns the rest of the file into one word, so it's a sensible guess. I verified this message only in the source and didn't run it as an example.

Easy to get wrong: the error's line number is "the line where reading stopped"

cf->conf_file->line goes up by one at every LF (lines 621–622). ngx_conf_log_error(), which prints errors, appends the file name and the line number at that moment as in %s:%ui, or in command line for the fake file (fd NGX_INVALID_FILE) (ngx_conf_file.c lines 1014–1027).

So if you forget a ;, the error points not to that line but to the line the parser had read down to while looking for a terminator. In section 6's example, ; was missing after worker_processes 2 on line 3, and line 4 was reported. Errors in the -g string end with "in command line" instead of a line number.

6. ngx_conf_handler() and the error messages

ngx_conf_handler() (lines 362–505) finds a directive by the first word of the word array and calls its function. The lookup is simple:

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

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

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

            /* ... continue unless the name has the same length and characters ... */

            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

It scans every module's directive table from the start, in order. No hash, no sorting. That differs from httpd, which puts directive names into a hash table and looks them up there (httpd Part 7). Config is read only at startup, reload, -t and -s, and there are a few hundred directives, so this simplicity was presumably judged good enough (the source doesn't state a reason).

When it finds a directive with the same name it sets found = 1, and if the module type or context bits don't match it keeps looking for the next candidate, because several modules can have the same name (for example, proxy_pass exists in both the HTTP proxy module and the stream proxy module). When nothing matches all the way to the end, found decides between two errors (lines 486–496).

The following lines 405–449 check the terminator ({ for block directives, ; for the rest) and the number of arguments (NGX_CONF_TAKE1, NGX_CONF_FLAG, NGX_CONF_1MORE …); lines 451–467 locate the config struct, and line 469 calls cmd->set(cf, cmd, conf). If set returns a string, that string becomes the error "name" directive <string> as-is (lines 479–480). The order of these checks and the pointer arithmetic that finds the config struct are covered in detail, with figures, in Part 5. Here we just list where each error message comes from.

The order in which ngx_conf_handler handles one directive, and where the errors branch off. It takes the first word of the word array as the directive name and scans every module's commands table from the start. If no ngx_command_t has that name all the way to the end (found = 0), the error is unknown directive (unknown in Example 3). If one does, it sets found = 1 and checks whether the module type and context bits (cf->module_type, cf->cmd_type) match; if not, it moves to the next candidate. If nothing matched by the end and found is 1, the error is directive is not allowed here (not-allowed in Example 3). On a match it checks the terminator: { for blocks, ; for everything else; if wrong, the error is is not terminated by ";" or has no opening "{" (no-semicolon in Example 3). Next, if the argument count doesn't fit the type bits, the error is invalid number of arguments (arg-count in Example 3). If everything fits, it finds the config struct and calls cmd->set(cf, cmd, conf); if set returns a string, the error is "X" directive followed by that string (duplicate in Example 3). On NGX_CONF_OK it moves on to the next directive. ngx_conf_handler() drawn in the order of its checks. Each red box names the Example 3 config below that produced that error.

Error message Printed at Meaning
unknown directive "X" ngx_conf_handler() lines 493–494 no module has that name (a typo, or the module wasn't built)
"X" directive is not allowed here lines 487–488 the name exists, but none fits the current context and module type
directive "X" is not terminated by ";" lines 406–408 a non-block directive ended with {
directive "X" has no opening "{" lines 413–415 a block directive ended with ;
invalid number of arguments in "X" directive lines 500–502 the argument count doesn't match the type bits
"X" directive is duplicate etc. lines 479–480 a string returned by the set function
unexpected "}", unexpected end of file, expecting "}" ngx_conf_parse() lines 268 and 278–279, ngx_conf_read_token() lines 691–692 braces don't pair up

Example 3: broken configs through nginx -t

Starting from one good config, I made several configs that are each broken in exactly one place and ran nginx -t on them. The example folder is ex/u04-config-errors. The base config is 15 lines — error_log and pid, events {}, and an http {} with one server { listen 127.0.0.1:8080; return 200 "ok\n"; } — and a broken line is inserted at line 3 (the main context), line 9 (inside http {}) or at the end (make_conf() in run.sh).

Name Inserted line
unknown line 3 worker_processess 2; (one extra s)
not-allowed line 9 (inside http {}) worker_processes 2;
no-semicolon line 3 worker_processes 2 (no ;)
arg-count line 3 worker_processes 1 2;
duplicate line 3 worker_processes 1; worker_processes 2;
bad-flag line 3 daemon maybe;
extra-brace one extra } at the end
missing-brace the last } removed
=== nginx -t: good ===
nginx: the configuration file /tmp/u04/good.conf syntax is ok
nginx: configuration file /tmp/u04/good.conf test is successful
=== nginx -t: unknown ===
nginx: [emerg] unknown directive "worker_processess" in /tmp/u04/unknown.conf:3
nginx: configuration file /tmp/u04/unknown.conf test failed
=== nginx -t: not-allowed ===
nginx: [emerg] "worker_processes" directive is not allowed here in /tmp/u04/not-allowed.conf:9
nginx: configuration file /tmp/u04/not-allowed.conf test failed
=== nginx -t: no-semicolon ===
nginx: [emerg] directive "worker_processes" is not terminated by ";" in /tmp/u04/no-semicolon.conf:4
nginx: configuration file /tmp/u04/no-semicolon.conf test failed
=== nginx -t: arg-count ===
nginx: [emerg] invalid number of arguments in "worker_processes" directive in /tmp/u04/arg-count.conf:3
nginx: configuration file /tmp/u04/arg-count.conf test failed
=== nginx -t: duplicate ===
nginx: [emerg] "worker_processes" directive is duplicate in /tmp/u04/duplicate.conf:3
nginx: configuration file /tmp/u04/duplicate.conf test failed
=== nginx -t: bad-flag ===
nginx: [emerg] invalid value "maybe" in "daemon" directive, it must be "on" or "off" in /tmp/u04/bad-flag.conf:3
nginx: configuration file /tmp/u04/bad-flag.conf test failed
=== nginx -t: extra-brace ===
nginx: [emerg] unexpected "}" in /tmp/u04/extra-brace.conf:15
nginx: configuration file /tmp/u04/extra-brace.conf test failed
=== nginx -t: missing-brace ===
nginx: [emerg] unexpected end of file, expecting "}" in /tmp/u04/missing-brace.conf:14
nginx: configuration file /tmp/u04/missing-brace.conf test failed
=== where the errors point: no-semicolon.conf lines 3-4 ===
worker_processes 2
events {
  1. not-allowed — the name worker_processes exists in ngx_core_module, so found = 1. But inside http {}, cf->module_type is NGX_HTTP_MODULE, so core module directives are skipped; nothing matches to the end, hence "not allowed here".
  2. no-semicolon — the error points to line 4. With no ;, the tokenizer keeps collecting words for the same directive past the end of the line up to events, and returns NGX_CONF_BLOCK_START at the {. The words became worker_processes, 2, events, and a non-block directive ended with {, hence "not terminated by ";"". The terminator check (line 405) comes before the argument count check (line 421 on), which is why you get this message rather than "invalid number of arguments".
  3. duplicate and bad-flag are strings returned by the directive function (set). "is duplicate" is returned by ngx_set_worker_processes() when the field is no longer NGX_CONF_UNSET (nginx.c lines 1631–1633), and the daemon message is printed directly by the standard setter ngx_conf_set_flag_slot() (ngx_conf_file.c lines 1055–1058).
  4. extra-brace was printed at line 268 of ngx_conf_parse() when it met a } at the outermost level of the file (parse_file); missing-brace was printed at lines 278–279 by the inner parser that was reading the http {} block (parse_block) when it hit the end of the file.

-g is read before the file

The second half of the same example is about -g.

=== -g 'worker_processes 3;' (file has none) ===
nginx: the configuration file /tmp/u04/good.conf syntax is ok
nginx: configuration file /tmp/u04/good.conf test is successful
=== -g 'worker_processes 3;' when the file also sets it ===
nginx: [emerg] "worker_processes" directive is duplicate in /tmp/u04/sets-one.conf:3
nginx: configuration file /tmp/u04/sets-one.conf test failed
=== -g without the semicolon ===
nginx: [emerg] unexpected end of parameter, expecting ";" in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -g with a block ===
nginx: [emerg] block directives are not supported in -g option in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -g with an http-level directive ===
nginx: [emerg] "gzip" directive is not allowed here in command line
nginx: configuration file /tmp/u04/good.conf test failed
=== -T dump shows the file, not -g ===
# configuration file /tmp/u04/good.conf:
error_log /tmp/u04/error.log;
pid /tmp/u04/nginx.pid;
  1. -g is not an option that "overrides values in the file". If the file also has worker_processes 1;, then -g 'worker_processes 3;' is read first and fills the value, and the file's line then fails with "is duplicate". The error points to the file (sets-one.conf:3). That's exactly the order from step ③ in section 3.
  2. Without the ;, reaching the end of the string with an fd of NGX_INVALID_FILE gives "unexpected end of parameter" (ngx_conf_read_token() lines 545–549). For a file, the same spot would say "unexpected end of file".
  3. A block is stopped by section 4's parse_param check (lines 288–293). So -g can only give main-context directives. gzip on; is an HTTP module directive, so in the main context it's "not allowed here".
  4. The nginx -T dump doesn't show -g. The dump collects content only when ngx_conf_parse() opens a file (ngx_conf_add_dump() at lines 222–237).

7. Inheriting from the old cycle — listening sockets and shared memory

On reload, nginx reads the config file again from scratch and builds a new cycle. Yet port 80 is never closed even for a moment, and neither the counters of limit_req_zone nor the key list of proxy_cache are lost. That's because steps ⑥ and ⑦ of ngx_init_cycle() look for the same thing in the old cycle and inherit it.

The old cycle and the new cycle. The old cycle has a listening array (127.0.0.1:8080, fd=6), shared_memory (zone one 10m) and open_files (access.log, fd=4). When the new cycle is built, 127.0.0.1:8080 in its listening array starts with fd=-1; if the address is the same, it takes over the old fd and marks remain=1 on the old one. The shared_memory zone one 10m takes over shm.addr if the name, tag and size are the same. access.log in open_files is opened afresh. Old sockets without remain, unused zones and old files are closed. The fd numbers and the zone name are just examples. Files are not inherited; they are opened afresh.

Listening sockets: same address, hand over the fd

        nls = cycle->listening.elts;
        for (n = 0; n < cycle->listening.nelts; n++) {

            for (i = 0; i < old_cycle->listening.nelts; i++) {
                /* ... skip if ignore, already remain, or a different socket type ... */

                if (ngx_cmp_sockaddr(nls[n].sockaddr, nls[n].socklen,
                                     ls[i].sockaddr, ls[i].socklen, 1)
                    == NGX_OK)
                {
                    nls[n].fd = ls[i].fd;
                    nls[n].previous = &ls[i];

                    if (ls[i].protocol != nls[n].protocol) {
                        nls[n].change_protocol = 1;

                    } else {
                        nls[n].inherited = ls[i].inherited;
                        ls[i].remain = 1;
                    }

src/core/ngx_cycle.c lines 519–548

Just before this (lines 513–517) the remain flag of every old socket is cleared to 0. Then, for each listen in the new config, it looks among the old sockets for one with the same address, port and socket type, takes its fd as-is, and marks the old one remain = 1 (keep). Next, ngx_open_listening_sockets() skips entries that already have an fd (ngx_connection.c lines 500–502) and runs socket(), bind() and listen() only for those with fd == -1 — only the addresses that are new. Finally the cleanup step (lines 719–751) closes only the old sockets without remain, i.e. the addresses that disappeared from the new config. The socket for the same port is never closed, so connections waiting in that socket's kernel queue aren't lost either.

Shared memory: same name, owner and size, reuse it

Shared memory zones follow the same idea (lines 440–491). For each new zone it scans the old zones, and if the name is the same, the tag (usually the address of the module struct that uses the zone) is the same and the size is the same, it takes over the old address (shm.addr) and passes the old data to the zone's init callback (lines 473–487). If the size differs, it allocates new memory (lines 493–503). So if you change limit_req_zone ... zone=one:10m to 20m and reload, that zone is created anew and the state it had built up is gone. I verified this only in the source and didn't run it as an example. The allocator inside shared memory (ngx_slab.c) got only a quick taste in Part 3.

Easy to get wrong: nginx -t tries to open the ports too

Since -t runs the same ngx_init_cycle() as startup to the end, -t also calls ngx_open_listening_sockets() at step ⑦. But if you run nginx -t on a server where nginx is already running, the port should be taken — and yet it doesn't fail. The reason sits right after bind():

            if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
                err = ngx_socket_errno;

                if (err != NGX_EADDRINUSE || !ngx_test_config) {
                    ngx_log_error(NGX_LOG_EMERG, log, err,
                                  "bind() to %V failed", &ls[i].addr_text);
                }

                /* ... close the socket ... */

                if (err != NGX_EADDRINUSE) {
                    return NGX_ERROR;
                }

                if (!ngx_test_config) {
                    failed = 1;
                }

                continue;
            }

src/core/ngx_connection.c lines 628–651

With -t, EADDRINUSE ("address already in use") is neither logged nor counted as a failure. Other errors (for example EACCES, when you lack permission to open port 80) fail under -t too. At startup, EADDRINUSE sets failed = 1, and the outer for (tries = 5; tries; tries--) loop (line 444) rests 500ms and tries again (lines 724–729). If all five tries fail, it's "still could not bind()".

Example 4: -t's bind(), and a port already in use

ex/u04-listen-test uses a config with nothing but listen 127.0.0.1:8080; to look at three things: ① the system calls of nginx -t under strace while the port is free, ② nginx -t while python3's http.server holds port 8080, ③ a real start in the same situation.

=== 1. port 8080 is free: strace nginx -t ===
nginx: the configuration file /work/nginx.conf syntax is ok
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 5
bind(5, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(5, 511)                          = 0
nginx: configuration file /work/nginx.conf test is successful
=== 2. another process holds 127.0.0.1:8080 ===
nginx: the configuration file /work/nginx.conf syntax is ok
bind(5, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EADDRINUSE (Address already in use)
nginx: configuration file /work/nginx.conf test is successful
=== 3. same situation, but really start nginx (stderr hidden, see error.log) ===
exit code: 1
took 2.5 s
=== error.log ===
2026/11/07 02:59:31 [emerg] 32#32: bind() to 127.0.0.1:8080 failed (98: Address already in use)
2026/11/07 02:59:31 [notice] 32#32: try again to bind() after 500ms
... (the same two lines three more times)
2026/11/07 02:59:31 [emerg] 32#32: bind() to 127.0.0.1:8080 failed (98: Address already in use)
2026/11/07 02:59:31 [notice] 32#32: try again to bind() after 500ms
2026/11/07 02:59:31 [emerg] 32#32: still could not bind()

Times and pids differ on every run. The error.log repeats the same two lines five times, so the middle is cut. ③'s config uses error_log … notice; so that the try again lines (NOTICE level) show up.

  1. -t really calls socket(), bind() and listen(). The socket is closed when the process exits.
  2. When the port is taken, bind() fails with EADDRINUSE, but -t is "successful". There's no error line either. That's why it's fine to run nginx -t next to a running server. Put the other way round, a successful -t doesn't guarantee that nginx can start on that port.
  3. The real start tried five times and gave up after about 2.5 seconds. That's 500ms × 5 (it rests once more after the last try before checking failed).

Where I got stuck: ③'s error.log lines were written over 2.5 seconds, yet they all show the same time, 02:59:31. nginx's log doesn't read the clock each time; it uses a cached time string (ngx_cached_err_log_time, src/core/ngx_log.c lines 117–118). That cache changes only when ngx_time_update() is called, and this retry loop just sleeps with ngx_msleep() (= usleep(), src/os/unix/ngx_time.h line 62) without updating the time. The time cache is covered with the event loop in Part 7.

8. reload — calling the same function again

Now we can follow a reload from start to finish. When you type nginx -s reload, two processes each call ngx_init_cycle().

The order of a reload. When you type nginx -s reload, a new process reads all of nginx.conf with ngx_init_cycle(). If the config has an error, it prints [emerg], exits with 1 and sends no signal. If the syntax is fine, it sends SIGHUP to the pid in the pid file. The master builds a new cycle with ngx_init_cycle(cycle). If the new cycle is NULL, it leaves an emerg in error.log and keeps running the old cycle. On success it starts new workers with ngx_start_worker_processes() and sends SIGQUIT to the old workers so they finish their current connections and exit. A config error is caught twice: once by the signaller, once by the master.

The master's code is in its loop, which wakes up after the signal arrives:

            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");

            cycle = ngx_init_cycle(cycle);
            if (cycle == NULL) {
                cycle = (ngx_cycle_t *) ngx_cycle;
                continue;
            }

            ngx_cycle = cycle;
            ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
                                                   ngx_core_module);
            ngx_start_worker_processes(cycle, ccf->worker_processes,
                                       NGX_PROCESS_JUST_RESPAWN);
            ngx_start_cache_manager_processes(cycle, 1);

            /* allow new processes to start */
            ngx_msleep(100);

            live = 1;
            ngx_signal_worker_processes(cycle,
                                        ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

src/os/unix/ngx_process_cycle.c lines 245–265

If ngx_init_cycle() returns NULL, it picks the global ngx_cycle (the old cycle) back up and does continue. As we saw in section 3, a failed ngx_init_cycle() never touched the old cycle, so the server keeps running with the old config as if nothing happened. On success it starts workers with the new cycle and sends NGX_SHUTDOWN_SIGNAL (SIGQUIT) to the old workers. The NGX_PROCESS_JUST_RESPAWN marker and how old workers finish their connections and exit are covered in Part 6.

Example 5: reloading with a broken config

This is the second half of the same folder as Example 1 (ex/u04-init-cycle-gdb). After swapping the workers once with a normal reload, I added a single line bogus_directive on; inside the config's events {} and then ① ran nginx -s reload, and ② sent kill -HUP directly, bypassing the signaller.

=== 1. start the master under gdb (daemon off) ===
master pid: 49   workers: 55 56 
=== 2. nginx -s reload, the signaller itself under gdb ===
...
workers after reload: 79 80 
=== 3. break the config, then nginx -s reload ===
nginx: [emerg] unknown directive "bogus_directive" in /tmp/u04g/nginx.conf:9
exit code: 1
=== 4. same broken config, but send SIGHUP directly ===
workers: 79 80 
curl: hello

The ... in step 2 is the gdb output shown in Example 1. Here is the relevant part of the master's error.log:

2026/11/07 02:54:26 [notice] 49#49: signal 1 (SIGHUP) received from 76, reconfiguring
2026/11/07 02:54:26 [notice] 49#49: reconfiguring
2026/11/07 02:54:26 [notice] 49#49: using the "epoll" event method
2026/11/07 02:54:26 [notice] 49#49: start worker processes
2026/11/07 02:54:26 [notice] 49#49: start worker process 79
2026/11/07 02:54:26 [notice] 49#49: start worker process 80
2026/11/07 02:54:26 [notice] 55#55: gracefully shutting down
2026/11/07 02:54:26 [notice] 56#56: gracefully shutting down
...
2026/11/07 02:54:28 [notice] 49#49: signal 1 (SIGHUP) received from 1, reconfiguring
2026/11/07 02:54:28 [notice] 49#49: reconfiguring
2026/11/07 02:54:28 [emerg] 49#49: unknown directive "bogus_directive" in /tmp/u04g/nginx.conf:9

Pids and times differ on every run.

  1. The SIGHUP of the normal reload was "received from 76" — the signaller's pid from Example 1. The workers changed from 55 and 56 to 79 and 80, and the old workers were "gracefully shutting down".
  2. With the broken config, nginx -s reload ended with exit 1, and nothing at all appeared in the master's log. Just as in section 1's "easy to get wrong", the signaller failed while reading the config and never sent the signal. The master was never even told to reload.
  3. Sending kill -HUP directly makes the master call ngx_init_cycle(), which fails ("received from 1" means the run.sh shell, pid 1 in the container, sent it). The log gets one emerg line after "reconfiguring", and the workers are still 79 and 80, and curl still answers hello. The continue at lines 245–250 kept the old cycle running. This was the third ngx_init_cycle() call in Example 1.

In short, the exit code and message of nginx -s reload are the first safety net, and the master's rollback is the second. If you reload with kill -HUP, bypassing the signaller, there's no first net, and a failure is recorded only in error.log.

Compared with httpd: how many times is the config read?

In httpd Part 5, httpd read its config twice at startup (a preflight read to check it, then again inside the loop). nginx's main() calls ngx_init_cycle() only once; in Example 1's master output there's just one call at startup. Both share the idea of handling a restart as "read the config from scratch and build a new generation" — httpd clears the pconf pool and goes around main()'s loop once more, while nginx builds a new cycle in a new pool. The difference is that nginx builds the whole new generation before discarding the old one, so if the new config is wrong it can keep running the old generation. That contrasts with httpd Part 5's "if the new config has an error, graceful brings the server down". Also, like httpd's -k, nginx's -s sends its signal only after reading the config — in both, the pid file's location lives in the config.

What this post left out

Summary

Concept In one line
main() options → an empty init_cycle on the stack → ngx_init_cycle() once → exit for -t, signal for -s, otherwise daemon, pid file and the master loop
ngx_cycle_t one set of configuration plus the resources opened for it (sockets, files, shared memory), all in one pool. Module configs live in conf_ctx[module index]
ngx_init_cycle(old) takes the old cycle and builds a new one. Startup (init_cycle) and reload (the running cycle) use the same function
Nine steps pool → create_conf → parse -g and the file → init_conf → files → shared memory → sockets → init_module → clean up the old cycle
Failure undo only what was newly made and return NULL. The old cycle is untouched, so the server keeps running when a reload fails
ngx_conf_parse() file, block and -g in one function. A block is recursion: the block directive's function changes cf and calls it again
Context cf->module_type + cf->cmd_type. The top bits overlap across module types (0x02000000), so both are checked together
ngx_conf_read_token() a hand-written, character-by-character state machine. 4KB buffer, words end in NUL, handles quotes, \, #, ${
ngx_conf_handler() scans every module's directive table in order. found separates unknown from not allowed here
-g read before the file, main context only, no blocks. If the file has the same directive, it's duplicate
Inheriting listening socket fds with the same address and shared memory with the same name, tag and size come from the old cycle
nginx -t runs the same function as startup to the end. It tries the ports too, but lets EADDRINUSE pass
nginx -s reload reads the whole config too. If it's broken, no signal is sent

What's next

Many places in this post just said "call the directive's function" and moved on. How conf and offset in ngx_command_t get the 2 of worker_processes 2; into ngx_core_conf_t.worker_processes, the order in which the http {} block creates and merges main, srv and loc configs for each module, and how to build your own dynamic module and load it with load_module continue in Part 5: Modules and Directives — How One Line of nginx.conf Becomes a Module's Config Struct. The other half of reload — the master starting new workers and the old workers finishing their connections and stepping down — is in Part 6: Master and Workers — Processes, Signals and Zero-Downtime Reload.

The comparison with httpd is in sections 4 and 8 (httpd Part 5, httpd Part 7). How Tomcat reads server.xml with Digester to build objects and starts up through Lifecycle's init() and start() is in Tomcat Part 3.

More posts