What I Learned Reading nginx.c
I've been working on a simple HTTP server in C - the kind that serves static files from a dist/ folder. It works, but it's naive. I wanted to see how the pros do it, so I started reading the nginx source code, where I began with src/core/nginx.c.
The Sentinel Pattern
Nginx relies heavily on static arrays for configuration commands, module definitions, and enum values. None of these structs carry an explicit .length field. Instead, Nginx relies on sentinel values to mark the end of arrays.
Here is a real example from nginx.c. The ngx_debug_points array maps config-file strings to enum values:
static ngx_conf_enum_t ngx_debug_points[] = {
{ ngx_string("stop"), NGX_DEBUG_POINTS_STOP },
{ ngx_string("abort"), NGX_DEBUG_POINTS_ABORT },
{ ngx_null_string, 0 } // <- the sentinel
};
Why I Liked It
In C, arrays don't carry their length at runtime. You usually have to, pass a separate length variable everywhere which is easy to get out of sync.
The sentinel is a contract: "if the name is empty/zero, you've reached the end." It's simple, it's hard to mess up, and it avoids a whole class of off-by-one bugs.
It's a small change but it makes the array self-contained.
Nginx's Memory Pool (ngx_pool_t)
This was the bigger revelation. My server uses malloc() and free() like a typical C program:
char *content = read_file(path, &size);
// ... send response ...
free(content); // <- what if I forget this? what if I return early?
nginx doesn't do this. Almost nowhere in the codebase will you find a raw free(). Instead, nginx uses memory pools (ngx_pool_t).
How the pool works
The model is clean and efficient:
- Create a pool with
ngx_create_pool(size, log). - Allocate from it using
ngx_palloc(pool, size)(orngx_pcalloc()for zeroed memory). - Never call
free()on individual allocations. - When request processing completes, destroy the pool with
ngx_destroy_pool(pool). Every block allocated inside that pool is freed at once.
// nginx style - no individual frees needed
ngx_pool_t *pool = ngx_create_pool(1024, log);
char *buf1 = ngx_palloc(pool, 256);
char *buf2 = ngx_pcalloc(pool, 512);
ngx_array_t *arr = ngx_array_create(pool, 10, sizeof(int));
// ... do work ...
ngx_destroy_pool(pool); // ONE call frees buf1, buf2, arr, and associated memory
Why This Matters
In a server that handles thousands of requests, you have two failure modes with traditional malloc/free:
- Memory leak: you allocate in a code path but forget to
free(). - Double free / use-after-free: you
free()something and then accidentally use it, orfree()it twice.
The pool eliminates both. You can't forget to free something that was allocated from a pool - the pool owns it. And you can't double-free because you never call free() at all.
Cool stuff.