Backend Development 10 min read

Understanding Nginx Architecture: Master/Worker Processes, Hot Reload, Epoll, and Load Balancing

The article explains Nginx’s master‑worker architecture, showing how the master process handles signals, restarts workers, and performs hot configuration or binary reloads, while workers use an event‑driven epoll loop with an accept‑mutex to efficiently process connections and balance load across cores without multithreading.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding Nginx Architecture: Master/Worker Processes, Hot Reload, Epoll, and Load Balancing

Introduction – The author recently started using the OpenResty framework built on Nginx and documented the learning outcomes.

1. Nginx Basic Architecture

Nginx runs as a daemon with one master process and multiple worker processes. The master manages workers, while workers handle network events. The design follows an event‑driven, asynchronous, non‑blocking model.

The advantages of this design are:

Full utilization of multi‑core CPUs, improving concurrency.

Load balancing among workers.

The master monitors workers, can restart failed workers, and supports hot configuration reloads and graceful upgrades, enhancing reliability and dynamic scalability.

2. Master Process

Core Logic

The master’s main loop is implemented in ngx_master_process_cycle . Key responsibilities include:

ngx_master_process_cycle(ngx_cycle_t *cycle)
{
    ...
    ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN);
    ...
    for (;;) {
        if (delay) {...}
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");
        sigsuspend(&set);
        ngx_time_update();
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "wake up, sigio %i", sigio);
        if (ngx_reap) {
            ngx_reap = 0;
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");
            live = ngx_reap_children(cycle);
        }
        if (!live && (ngx_terminate || ngx_quit)) {...}
        if (ngx_terminate) {...}
        if (ngx_quit) {...}
        if (ngx_reconfigure) {...}
        if (ngx_restart) {...}
        if (ngx_reopen) {...}
        if (ngx_change_binary) {...}
        if (ngx_noaccept) {
            ngx_noaccept = 0;
            ngx_noaccepting = 1;
            ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
        }
    }
}

From the code we can see that the master process mainly:

Receives external signals (e.g., ngx_quit for graceful shutdown).

Sends signals to workers (e.g., ngx_noaccept corresponds to WINCH, telling workers to stop accepting new connections).

Monitors worker status (e.g., ngx_reap handles unexpected worker exits).

Automatically restarts workers that exit abnormally.

Hot Reload / Hot Upgrade

Configuration hot‑reload – Updating nginx.conf and sending SIGHUP (or nginx -s reload ) makes the master load the new configuration, start new workers with it, and let old workers finish existing connections before exiting.

Binary hot‑upgrade – Replacing the Nginx binary and sending USR2 to the master creates a new master/worker pair while the old ones keep running. After verifying the new workers, the old master receives QUIT to shut down; if the upgrade fails, a rollback is performed using HUP and QUIT signals.

3. Worker Process

Core Logic

ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ngx_int_t worker = (intptr_t) data;
    ngx_process = NGX_PROCESS_WORKER;
    ngx_worker = worker;
    ngx_worker_process_init(cycle, worker);
    ngx_setproctitle("worker process");
    for (;;) {
        if (ngx_exiting) {...}
        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");
        ngx_process_events_and_timers(cycle);
        if (ngx_terminate) {...}
        if (ngx_quit) {...}
        if (ngx_reopen) {...}
    }
}

The worker mainly processes network events via ngx_process_events_and_timers , handling both connection and timer events.

Event‑driven epoll

Workers use the epoll model to manage massive concurrent connections efficiently. Epoll separates connection events from read/write events, storing them in different queues, and uses an accept‑mutex to avoid the “thundering herd” problem.

void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    // ... handle listening sockets ...
    if (ngx_accept_disabled > 0) {
        ngx_accept_disabled--;
    } else {
        if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
            return;
        }
        // ... set flags for post events ...
    }
    // epoll_wait for network events
    // push connection events to ngx_posted_accept_events
    // push read/write events to ngx_posted_events
    (void) ngx_process_events(cycle, timer, flags);
    // process connection events only if lock held
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
    // process read/write events
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

To solve the thundering‑herd issue, Nginx:

Separates connection events ( ngx_posted_accept_events ) from read/write events ( ngx_posted_events ).

Uses ngx_accept_mutex so only the process holding the lock can accept new connections.

4. Load Balancing

Workers balance load based on the number of active connections. The threshold ngx_accept_disabled is calculated as:

ngx_int_t ngx_accept_disabled;
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;

When a worker’s active connections reach 7/8 of the total, it stops accepting new connections until the count drops below the threshold, allowing other workers to take over.

5. Reflections

Why not use a multithreaded model?

Nginx is a stateless service; processes do not need to share memory.

Process isolation prevents a crash in one worker from affecting others, improving reliability.

No shared resources means no locking overhead.

Why not use multithreading for business logic?

Number of processes already matches CPU cores; adding threads would increase context‑switch cost.

The access layer mainly forwards data; its I/O is already non‑blocking and event‑driven, so extra threads bring little benefit. Blocking logic should be handled at the application level (e.g., Lua coroutines in OpenResty).

Author Bio

Li Shuai, backend engineer at Tencent, works on the esports side of “Peace Elite”. Graduated from Sichuan University, focuses on Lua and network frameworks.

backend developmentnginxMaster Processhot reloadepollLoad BalancingWorker Process
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.