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:
- What does
main()do, and where donginx -tandnginx -s reloadbranch off? - Why does
ngx_init_cycle()take an "old cycle" and build a "new cycle"? - How is one line of nginx.conf split into words, and how does the parser get inside a block like
events {}? - Where do error messages like
unknown directiveandis not allowed herecome from, and what does the line number point to?
| 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".
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".
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():
ngx_os_init()— reads operating-system values like the page size, the number of CPUs (ngx_ncpu, used byworker_processes auto) and the file descriptor limit.ngx_add_inherited_sockets()(lines 482–538) — if the environment variableNGINXlists socket numbers, it puts those sockets intoinit_cycle.listening. This path is only used when the binary is replaced while running (USR2); normally it returns right away at lines 488–492. Binary replacement is covered in Part 6.ngx_preinit_modules()— numbers (index) and names the static module arrayngx_modules[]that configure generated (src/core/ngx_module.clines 26–39). That's the topic of Part 5.
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
nginx -tprints "test is successful" and exits ifngx_init_cycle()succeeds. So the test that-tperforms is running the same function as startup all the way through. The "syntax is ok" line printed just before is printed insidengx_init_cycle(), right after the config file has been read (ngx_cycle.clines 292–295). What happens between those two lines (opening log files, shared memory, listening sockets) is covered in sections 3 and 7.nginx -s reloadgoes tongx_signal_process()(ngx_cycle.clines 1097–1147). It gets the pid file path from the config, reads the number in that file, andngx_os_signal_process()looks up the name"reload"in thesignals[]table and callskill(pid, SIGHUP)(src/os/unix/ngx_process.clines 632–648).- Everything else is the server. It installs signal handlers (
ngx_init_signals()), forks withngx_daemon()to detach from the terminal ifdaemon on(the default), writes the pid file (line 382), and entersngx_master_process_cycle()ifmaster_process on(the default) (lines 399–404). Part 6 picks up the master's loop.
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 |
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.
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.
- ⑤ The pid file (only with
-t, or when a reload changed the pid path, lines 322–348), a lock file test, creating paths likeproxy_temp_path, and thenopen()on each log file piled up inopen_files(lines 365–409). After that,cycle->log = &cycle->new_log;(line 411) switches from the startup log to nginx.conf'serror_log. - ⑥ For each zone in the
shared_memorylist, it gets shared memory (lines 415–508). - ⑦ It opens the sockets in the
listeningarray (lines 511–638).
⑥ 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 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.
- The first call comes from line 300 of
main(), withold_cycle->conf_ctx=(nil)— the old cycle isinit_cycleon the stack (the0x7fff…address is in the stack region). At this pointngx_processis still 0 (NGX_PROCESS_SINGLE), because it only becomes master after the config is read, at lines 346–348 ofmain(). - 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 withconf_ctxfilled in. One function, with a single shape — "take the old, build the new" — handles both startup and reload. - 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. - The signaller (pid 76) calls
ngx_init_cycle()too. Withngx_process=2(NGX_PROCESS_SIGNALLER) it returns at step ④'s exit.
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:
- a
}outside a block →unexpected "}" - the file ends inside a block →
unexpected end of file, expecting "}" - a
{in-g→block directives are not supported in -g option
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
- Copy the whole current parser state
*cfinto the local variablepcf. - Change the context — the place to fill (
ctx) becomes the event modules' config array, the module typeNGX_EVENT_MODULE, the context bitsNGX_EVENT_CONF. ngx_conf_parse(cf, NULL)— no file name and a file currently open, so this isparse_block. It keeps reading the same buffer of the same file and returns at the matching}.- Restore the original context with
*cf = pcf. The next directive is read in the main context again.
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 {).
- The first line is
daemon offfrom-g, at line number 0 — it is read before the file (step ③ in section 3).ngx_conf_param()sets the fake file'slineto 0 (line 91). - Block directives (
events,http,server,location) havelast=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 thelocation /block ended,location /vwas read in04000000(the server context) again. module_typereads as letters. The module type constants are four characters packed into an integer —NGX_CORE_MODULEis0x45524F43 /* "CORE" */(ngx_conf_file.hline 70). On a little-endian machine, reading the bytes in memory order gives "CORE".- Spaces and
;inside quotes don't split words, the quotes themselves are dropped, and\"becomes"—<a b; c "quoted">. The{in${uri}xwasn'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 |
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
- A
;or{with no word before it (as in;;) isunexpected ";". }ends a block only when there are no words yet. If you forget the;and writeworker_processes 1 }, it'sunexpected "}".#is a comment only where a word would start. Thisswitchis reached only whenlast_spaceis set, so a#in the middle of a word, as inabc#def, is an ordinary character.
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.
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 {
not-allowed— the nameworker_processesexists inngx_core_module, sofound = 1. But insidehttp {},cf->module_typeisNGX_HTTP_MODULE, so core module directives are skipped; nothing matches to the end, hence "not allowed here".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 toevents, and returnsNGX_CONF_BLOCK_STARTat the{. The words becameworker_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".duplicateandbad-flagare strings returned by the directive function (set). "is duplicate" is returned byngx_set_worker_processes()when the field is no longerNGX_CONF_UNSET(nginx.clines 1631–1633), and thedaemonmessage is printed directly by the standard setterngx_conf_set_flag_slot()(ngx_conf_file.clines 1055–1058).extra-bracewas printed at line 268 ofngx_conf_parse()when it met a}at the outermost level of the file (parse_file);missing-bracewas printed at lines 278–279 by the inner parser that was reading thehttp {}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;
-gis not an option that "overrides values in the file". If the file also hasworker_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.- Without the
;, reaching the end of the string with an fd ofNGX_INVALID_FILEgives "unexpected end of parameter" (ngx_conf_read_token()lines 545–549). For a file, the same spot would say "unexpected end of file". - A block is stopped by section 4's
parse_paramcheck (lines 288–293). So-gcan only give main-context directives.gzip on;is an HTTP module directive, so in the main context it's "not allowed here". - The
nginx -Tdump doesn't show-g. The dump collects content only whenngx_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 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.
-treally callssocket(),bind()andlisten(). The socket is closed when the process exits.- When the port is taken,
bind()fails withEADDRINUSE, but-tis "successful". There's no error line either. That's why it's fine to runnginx -tnext to a running server. Put the other way round, a successful-tdoesn't guarantee that nginx can start on that port. - 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().
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.
- 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".
- With the broken config,
nginx -s reloadended 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. - Sending
kill -HUPdirectly makes the master callngx_init_cycle(), which fails ("received from 1" means therun.shshell, 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 answershello. Thecontinueat lines 245–250 kept the old cycle running. This was the thirdngx_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
- The pointer arithmetic that finds a config struct's slot from
set,confandoffsetinngx_command_t(NGX_DIRECT_CONF,NGX_MAIN_CONF,cf->ctx + cmd->conf):ngx_conf_file.clines 451–467 — Part 5 - Create, merge and postconfiguration in the
http {}block:ngx_http_block()insrc/http/ngx_http.c(from line 123) — Part 5 includeand globs (*.conf):ngx_conf_include(),ngx_conf_file.clines 827–889cf->handler, which lets a block liketypes {}read its contents its own way instead of as directives:ngx_conf_parse()lines 298–322ngx_conf_add_dump(), which collects thenginx -Tdump (lines 105–157). A--with-debugbuild collects the dump even without-T(#if (NGX_DEBUG) || 1at lines 226–228)- Environment variable handling (the
envdirective,ngx_set_environment()):nginx.cfrom line 542 - Binary replacement (USR2) and inheriting sockets through the
NGINXenvironment variable:ngx_exec_new_binary()(nginx.cline 720),ngx_set_inherited_sockets()(ngx_connection.cline 135) — Part 6 - The Control API new in this line (1.31.x) (
--with-control-api, the-loption;src/os/unix/ngx_control.ccallsngx_init_cycle()as well). 1.30.6'sauto/optionshas no such option, and this post's build didn't enable it ngx_clean_old_cycles(), which cleans up old cycles every 30 seconds undermaster_process off:ngx_cycle.cfrom line 1380
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.
- Part 2: The Big Picture — Master and Workers, Modules, the Cycle and Config Contexts
- Part 3: Memory Pools and Core Data Structures — ngx_pool_t, ngx_str_t, ngx_buf_t and ngx_chain_t
- Part 7: The Event Loop — epoll, Timers, Posted Events and accept
- httpd Part 5: main() and Startup — Why the Config Is Read Twice, and Restarts