Visual-first learning · Knowledge base

NGINX Fundamentals

The front door of the modern web — why it exists, what it replaced, and the file-descriptor & epoll machinery that lets one worker shepherd thousands of connections at once.

Level: beginner → mechanism Anchor analogy: the restaurant Facts cited inline

01

The big picture

NGINX (“engine-x”) is software that sits in front of your web application and is the first thing every incoming request hits.

It does a handful of front-door jobs — handing out static files itself, passing harder requests to your real application, spreading load across multiple app servers, caching answers, and terminating HTTPS — while juggling a huge number of simultaneous visitors.

Anchor analogy · the restaurant

NGINX is the maître d’ / front-of-house host at the door. Guests are browsers, phones and API clients. The kitchens out back are your actual application servers. We reuse this one restaurant for the entire topic — one consistent model sticks; a pile of disconnected ones doesn’t.

                      THE INTERNET  (a crowd of guests arriving)
          browser   phone    API client   browser
             |        |           |           |
             v        v           v           v
   +-----------------------------------------------+
   |                  N G I N X                     |   <- the maitre d'
   |   - hands out static files itself (bread)      |
   |   - routes harder requests to the back         |
   |   - load-balances across kitchens              |
   |   - caches, terminates TLS                      |
   +-----------------------------------------------+
             |             |              |
             v             v              v
      +-----------+  +-----------+  +-----------+
      |  app srv  |  |  app srv  |  |  app srv  |       <- the kitchens / chefs
      |  (Node)   |  |  (Python) |  |  (Ruby)   |
      +-----------+  +-----------+  +-----------+

One-liner: NGINX is the front door and traffic director for a web service.

02

Why NGINX exists — the C10k problem

Rewind to the late 1990s / early 2000s. The dominant web server was Apache HTTP Server, and its classic model handled visitors with one dedicated worker (process, later thread) per connection.

Analogy

Imagine assigning one personal waiter to each guest for their whole visit — and that waiter just stands at the table while the guest reads the menu, chews, stares out the window. Each waiter costs a salary (memory) whether working or idle. With 50 guests, fine. With 10,000, the building can’t hold the waiters. The restaurant collapses — not because the kitchen is slow, but because you ran out of room for waiters.

That failure has a name: the C10k problem — can one server handle ten thousand concurrent connections? Coined by Dan Kegel around 1999.

   OLD MODEL  (thread/process per connection - classic Apache)

   guest 1     -->  worker 1   (idle, but holding memory + a stack)
   guest 2     -->  worker 2   (idle, blocked, waiting on the network)
   guest 3     -->  worker 3
     :                 :
   guest 10000 -->  worker 10000   <-- server falls over here
                                        not from CPU - from the
                                        overhead of 10k workers
Name the trap

Most people assume a slow server is short on CPU or bandwidth. The C10k insight is the opposite — at high concurrency the bottleneck is per-connection overhead (memory per thread/process + the cost of the OS switching between thousands of them). The connections are mostly just sitting there waiting on a slow network, and you pay full price for every idle one.

Igor Sysoev wrote NGINX to solve exactly this for a large Russian site (Rambler); first released 2004. The fix: a tiny number of workers — roughly one per CPU core — each watching thousands of connections and only doing work when a connection actually has something to do.

   NEW MODEL  (event-driven, non-blocking - NGINX)

   guest 1  -+
   guest 2  |
   guest 3  +-->  ONE waiter  --> watches ALL tables at once;
     :      |     (per CPU core)    springs into action only when
   guest N  -+                       a table actually raises a hand
Analogy stops here · what’s literally true

A real waiter can only watch so many tables. NGINX’s “watching” is done by the operating system’s event-notification machinery (epoll on Linux, kqueue on BSD/macOS), which is what makes “watch thousands at once” actually true. That machinery is the subject of sections 8–13.

Confidence: high on the C10k concept, NGINX’s 2004 authorship by Sysoev, and the event-driven vs per-connection contrast. No specific memory figures given — those are illustrative and vary by setup.

03

The alternatives & trade-offs

NGINX is not the only front door in town. The real contenders, and where each one actually wins:

ToolWhat it isSweet spot
Apache HTTP ServerThe incumbent web serverPer-directory config (.htaccess), running app code in-process (mod_php), huge module ecosystem, shared hosting
HAProxyDedicated proxy / load balancerHeavy-duty L4/L7 load balancing; fine-grained traffic control
CaddyModern web server (Go)Automatic HTTPS out of the box, dead-simple config
EnvoyCloud-native proxyService meshes, dynamic discovery, rich observability (microservices)
TraefikModern reverse proxy (Go)Auto-config from container labels (Docker/Kubernetes)
Cloud LBs (AWS ALB/ELB…)Managed service“I don’t want to run a box at all”
Just your app serverNode/Python serve HTTP directlyTiny projects, local dev

The honest axes

NGINX vs Apache. NGINX wins on raw concurrency, static-file serving, and low-memory proxying. Apache wins on flexibility: .htaccess lets each directory carry its own config (great for shared hosting), and it can run application code inside the server process. NGINX deliberately has no .htaccess equivalent — all config is centralized. That’s a feature (no per-request disk lookups) and a limitation (less convenient when you can’t touch main config).

NGINX vs HAProxy. HAProxy is a sharper pure load balancer with deeper traffic-shaping, but it won’t serve static files or be a general web server. NGINX is the Swiss Army knife; HAProxy is the specialist scalpel.

NGINX vs Caddy. Caddy gives automatic HTTPS with near-zero config. NGINX is more battle-tested and has a deeper ecosystem, at the cost of more setup.

NGINX vs Envoy/Traefik. In a dynamic container/microservices world where backends appear and vanish, Envoy and Traefik were built for auto-discovery. NGINX can do it but it’s not its native home.

Worth knowing: Apache later added an event MPM (a more NGINX-like model), default-ish in Apache 2.4 (~2012 — verify exact version if it matters). The gap is narrower today than the 2004 story suggests.

04

When to use NGINX — and when not to

Reach for NGINX when

You want a reverse proxy in front of one or more app servers; you serve a mix of static assets and dynamic responses; you need TLS termination; you want to load-balance across a handful of backends; or you want caching. In short: the default sensible front door for most web stacks.

Pick something else when

Red flagBetter tool
You want zero-config automatic HTTPSCaddy
Service mesh with dynamic discovery + deep observabilityEnvoy (often via Istio)
You depend on .htaccess / shared-hosting conventionsApache
You need the most advanced load-balancing knobsHAProxy
Tiny project, app server already serves HTTP fineAdd nothing — NGINX now is premature complexity

05

What .htaccess actually is

.htaccess (“hypertext access”) is an Apache feature: a small config file you drop inside a directory of your site. It applies rules to that directory and everything below — URL rewrites, redirects, password-protecting a folder, blocking IPs, custom error pages.

Analogy

A little house-rules card taped inside each room of the restaurant — “members-only,” “send these guests upstairs.” Anyone can write a card and tape it up without bothering management.

Why it exists: shared hosting. If you’re one of 500 customers on a provider’s Apache box, you can’t touch the main server config — so .htaccess lets you configure your slice by dropping a file in your folder, no admin access, no restart.

   APACHE with .htaccess enabled - EVERY request for
   /var/www/shop/cart/index.html makes Apache walk the path
   and read a card in each room along the way:

      /var/www/.htaccess           <- read it
      /var/www/shop/.htaccess      <- read it
      /var/www/shop/cart/.htaccess <- read it
                                     ...on EVERY request
Fact

Apache’s own documentation says: don’t use .htaccess if you have access to the main config — for exactly this performance reason (re-reading cards on every request) plus security. (httpd.apache.org/docs/2.4/howto/htaccess.html, “When (not) to use”.)

06

.htaccess: NGINX vs HAProxy

The twist: neither NGINX nor HAProxy has .htaccess — it’s an Apache concept. But why each lacks it is completely different, and that difference teaches you what each tool fundamentally is.

Serves files from directories?Per-directory runtime config?Where config lives
ApacheYesYes — .htaccessMain config + scattered .htaccess cards
NGINXYesNo, on purposeOne central config (nginx.conf + includes)
HAProxyNoN/A — concept doesn’t applyOne central config (haproxy.cfg)
Name the trap

Comparing NGINX and HAProxy on .htaccess is a low-key category error. NGINX is a web server that serves files, so it had a real choice and deliberately refused the per-directory model: all rules go in centralized config, re-read only on an explicit reload (nginx -s reload), never per-request. HAProxy doesn’t serve files at all — it’s a pure proxy/load balancer with no document root. There are no “rooms” to tape cards in; the question doesn’t even land on it.

NGINX = “No cards in rooms. One master rulebook at the door, memorized; re-memorized only when handed an update.” Faster (no per-guest card-reading), less convenient (edit the master rulebook).

HAProxy = the valet/dispatcher on the curb waving cars toward different restaurants. He has no dining room, so “house-rules cards” is a meaningless idea to him. (Tiny caveat: HAProxy can return a few canned error-page files via errorfile, but it has no document root or general static serving — so the .htaccess model still doesn’t apply.)

07

12 real-world use cases: .htaccess → NGINX

Same job, two filing systems. Syntax is illustrative — recognize the shape, look up exact flags when you ship.

#GoalApache .htaccess (module)NGINX equivalent
1Password-protect a folderRequire valid-user + AuthUserFile (mod_auth_basic)auth_basic + auth_basic_user_file
2301 redirect (old → new)Redirect 301 /old /new (mod_alias)return 301 /new;
3Force HTTPSRewriteCond %{HTTPS} off + RewriteRulereturn 301 https://$host$request_uri;
4Pretty URLs / front-controllerRewriteRule . index.php (mod_rewrite)try_files $uri $uri/ /index.php?$args;
5Custom error pagesErrorDocument 404 /404.htmlerror_page 404 /404.html;
6Block / allow IPsRequire not ip 1.2.3.4 (2.4)deny 1.2.3.4; / allow ...;
7Disable directory listingOptions -Indexesautoindex off; (already the NGINX default)
8Browser cache lifetimeExpiresByType image/png "access plus 30 days"expires 30d;
9Gzip compressionAddOutputFilterByType DEFLATE text/htmlgzip on; gzip_types ...;
10Hotlink protectionRewriteCond %{HTTP_REFERER} ...valid_referers ...; + if ($invalid_referer)
11Block bad botsRewriteCond %{HTTP_USER_AGENT} BadBotif ($http_user_agent ~* badbot){return 403;}
12Security header (HSTS)Header set Strict-Transport-Security ...add_header Strict-Transport-Security "...";
The structural constant across every row

The auth mechanism, redirect, gzip — these are HTTP-level ideas both servers implement. What changes in every single row is where the rule lives and when it’s read: Apache reads .htaccess on every request; NGINX reads central config once per reload. Switching servers mostly means relearning the filing system, not the web concepts.

Two honest footnotes: #4 isn’t a 1:1 translation — NGINX prefers try_files (does the file exist? else hand to the app) and treats if/rewrite as a last resort (see the famous “If Is Evil” community page). You rethink Apache rewrites into try_files rather than porting them. #7: directory listing is work to disable in Apache but is already off by default in NGINX.

Worked example · HTTP Basic Auth

Both servers use a near-identical .htpasswd-style file because the underlying scheme is the same web standard: HTTP Basic Authentication, RFC 7617. Apache: tape AuthType Basic + Require valid-user inside /admin. NGINX: add a location /admin/ { auth_basic ...; } block to central config, then nginx -s reload once. Same auth standard, different filing system.

08

File descriptors — the foundation

Before epoll makes sense, you need this brick. When you check a coat, they don’t pass the actual coat back and forth — they give you a numbered ticket, “#42.” From then on you say “42,” not “the navy wool one with the torn pocket.” The number is a handle — a tiny stand-in for a big complicated thing.

A file descriptor (FD) is that ticket, inside a program on Linux. And the surprising part: on Linux almost everything is treated like a “file” — not just disk files, but network connections, your keyboard, the screen, pipes between programs. Each time a program opens one, the OS hands back a small integer (0, 1, 2, 3…). That integer is the file descriptor.

   THE COAT CHECK  (analogy)        -->   YOUR PROGRAM (real)

   "here's ticket #42"              -->   open()/accept() returns FD 7
   you hold ticket #42              -->   program holds the integer 7
   "fetch 42" (not "the navy coat") -->   read(7) / write(7)
   attendant knows what #42 is      -->   kernel knows FD 7 =
                                          "TCP connection to 1.2.3.4"

So when a browser connects, NGINX gets back a file descriptor — say FD 7 — and that number now means “this visitor’s connection.” 10,000 visitors = 10,000 tickets = file descriptors 7, 8, 9, … up into the thousands.

Myth to kill

“File descriptor” sounds like it’s only about files on disk. It isn’t — a network connection to a browser is also behind a file descriptor. That “everything is a file” design is why one mechanism can watch disk files and network connections alike.

Technical version: an FD is a non-negative integer index into a per-process table the kernel keeps; each entry points to an open “file” object (disk file, socket, pipe…). FDs 0/1/2 are conventionally standard in/out/error. Confidence: high — settled Unix/Linux fundamentals (open(2)).

09

epoll vs select/poll — watch many, pay for few

NGINX is holding thousands of file descriptors. The question is: which of them have something ready right now? How you answer that is the whole ballgame.

The naive way: walk the whole room (select / poll)

Analogy

The waiter wants to know which tables need service, so he physically walks past every table, every round, asking “anything?” — including the 9,995 idle ones. He spends all his energy checking, not serving.

That’s literally select() / poll(): you hand the kernel your whole set of connections, it scans all of them, and you scan them again to find the ready ones. Cost grows with total connections — O(n) — on every call. (select also has a hard descriptor cap, commonly 1024.)

The smart way: a bell system (epoll)

Analogy

Install a bell at every table. Tables ring when they need something. The kitchen hands the waiter a list of exactly which tables rang — nothing else. Idle tables cost zero attention. Add 5,000 more idle tables? His workload doesn’t change — only the ringing ones do.

That’s epoll, Linux’s I/O event-notification mechanism (added in Linux 2.6, ~2002). You register FDs once; the kernel maintains a ready list behind the scenes; epoll_wait returns just the active ones. Cost scales with busy connections, not total.

Watch it: idle FDs are free under epoll

epoll_wait() returns → 
Twelve connections (file descriptors) registered on one epoll instance. Most sit idle and dark — costing nothing. Only the ones that “ring” (turn coral) land on the ready list that epoll_wait hands back. Add a thousand more dark cells and the work below doesn’t change. (select/poll would have to scan all twelve — and all thousand — every call.)
Name the trap

It’s tempting to credit NGINX’s speed to “it’s written in C” or “it’s just optimized.” That’s not the core. The core is the algorithmic difference between O(active) and O(total): a beautifully optimized select-based server still degrades as idle connections pile up, because its cost model is wrong. NGINX wins by changing the cost model.

NGINX doesn’t hard-code epoll — it auto-selects the best mechanism per OS:

OSEvent mechanism
Linuxepoll
FreeBSD / macOSkqueue
Solaris/dev/poll, event ports
(fallback)select / poll
Confidence: high on what epoll is, the O(n)→O(active) shift, and per-OS auto-selection. Medium / please-verify: the exact FD_SETSIZE (~1024) and whether NGINX defaults to epoll’s edge-triggered mode — both flagged rather than asserted.

10

How epoll works — the kernel does the noticing

The crucial thing: epoll is passive. It is not running around poking each connection. The thing doing the noticing is the kernel (the OS itself), while NGINX is busy elsewhere.

   When data comes back on a connection:

   1. data packet arrives at the network card
   2. the kernel's networking code: "this belongs to socket FD 7"
   3. kernel drops the data in FD 7's receive buffer and
      flips a flag: "FD 7 is now READY to read"
   4. because FD 7 is registered with the epoll instance,
      the kernel adds FD 7 to that instance's READY LIST
   5. NGINX calls epoll_wait()  -->  kernel hands back: [ 7 ]
   6. NGINX: "oh, FD 7 has data" and reads it

Steps 1–4 happen on their own while NGINX does nothing. By the time NGINX calls epoll_wait, the answer is already sitting there. NGINX doesn’t search — it collects. The three commands:

CallPlain EnglishWhen
epoll_create1()set up my bell-boardonce
epoll_ctl()wire up / unwire a table’s bell (add/remove an FD)connection opens or closes
epoll_wait()show me which bells are ringing right nowthe hot loop
Direction matters

epoll doesn’t get “attached” to connections — connections get registered onto the epoll instance via epoll_ctl(EPOLL_CTL_ADD). The board is the thing; FDs are added to it.

11

The complete request round trip

Now the payoff — one full request, browser knocks to browser answered. Notice both the browser’s connection (FD 7) and the backend’s connection (FD 51) live on the same epoll instance. One worker, one board, both directions. Press play to step through 1 → 7.

browser the guest · FD 7 backend the kitchen · FD 51 your server machine — one worker, one CPU core network card (NIC) the one physical door — all traffic in and out kernel — epoll ready list the bell-board: holds only FDs ready right now FD 7 · browser FD 51 · backend NGINX worker calls epoll_wait(), then reads / writes 1 7 4 5 2 3 6
Press play to trace one request from browser to backend and back.
StepWhat happens
1Browser knocks. Request lands on the NIC; this connection is FD 7.
2Kernel notices, not NGINX. It buffers the data, flips FD 7’s ready flag, puts it on the epoll ready list.
3epoll_wait collects. Returns [FD 7]; the worker reads the request.
4Worker forwards upstream. Opens FD 51 to the backend, sends the request, then goes back to watching the board — it does not freeze.
5Backend answers. Result arrives at the NIC; kernel marks FD 51 ready.
6epoll_wait collects again. Returns [FD 51]; the worker reads the result.
7Worker replies. Writes the response out to FD 7; the browser has its answer.
Two honest caveats so the diagram doesn’t lie

The analogy stops here: FD 51’s traffic to the backend also re-enters the kernel and exits via the NIC — there’s no separate door. The sides are split only so both FDs are visible.

Step 5 hides a fork: if the backend is slow, the worker doesn’t wait at step 4 — it parks FD 51 on the board and serves everyone else. That “don’t block, register and move on” behaviour is non-blocking I/O, and it’s what keeps steps 4→6 from freezing the dining room.

12

“The board” = an epoll instance

The board is an epoll instance, created with epoll_create1(). The twist that should make you grin: the board is itself a file descriptor. Create one and the kernel hands you back… an FD referring to it. The bell-board gets its own ticket number, just like every connection.

Inside that one instance, the kernel keeps two lists (the real terms):

   ONE epoll instance (one board) = one FD, holding two lists:

   +- epoll instance (itself FD 12) ---------------+
   |  INTEREST LIST  (everything I'm watching)      |
   |     FD 7   FD 8   FD 9 ... FD 51 ... FD 4287   |
   |                                                |
   |  READY LIST  (only what's ringing now)         |
   |     FD 7   FD 51        <- epoll_wait returns these
   +------------------------------------------------+

The interest list is everything you’ve registered via epoll_ctl (“all the tables whose bells I’m wired to watch”). The ready list is the subset ready right now (“the bells currently ringing”) — and epoll_wait pulls from that one.

Confidence: high. From epoll(7): “epoll_create() creates a new epoll instance and returns a file descriptor referring to that instance.”

13

Can there be many boards? Yes — one per worker

Sense 1 (trivial): nothing stops a program from calling epoll_create1() several times; each is an independent instance with its own interest/ready lists.

Sense 2 (the NGINX reality): NGINX runs as one master process + several worker processes, typically one worker per CPU core (worker_processes auto;). Each worker has its own epoll instance.

   4 cores  -->  4 workers  -->  4 boards  (NOT 4000 boards for 4000 connections)

   Worker 1 (core 1)        Worker 2 (core 2)
   +- board A (FD) -+        +- board B (FD) -+
   | FD 7, 9, 22... |        | FD 8, 15, 51...|
   +----------------+        +----------------+

   Worker 3 (core 3)        Worker 4 (core 4)
   +- board C ------+        +- board D ------+
   | FD 11, 30...   |        | FD 13, 41...   |
   +----------------+        +----------------+
Analogy

The dining room has 4 waiters, one per section, each with their own bell-board watching only their tables. A given table’s bell is wired to exactly one waiter’s board — not all four.

Carve this in stone

Number of boards ≈ number of workers (≈ number of CPU cores) — NOT number of connections. Each connection is a ticket on exactly one board. Thousands of FDs, a handful of boards.

Confidence: high on the master/worker model and one epoll-per-worker. Exact worker count depends on worker_processes and core count — auto ties it to cores, but an admin can override. Footnote: because the board is itself an FD, you can even nest one epoll inside another — real, occasionally used, not a beginner concern.

14

Here be dragons — edges, wrinkles, open threads

The clean model above is true, but reality has texture. These are the known wrinkles, flagged honestly so you know where to dig later.

WrinkleThe short versionConfidence
The slow-disk asymmetryNetwork sockets are non-blocking, but disk reads on Linux have historically been blocking in a way sockets aren’t. A giant file off a slow disk can stall a worker. This is why sendfile, aio, and thread pools exist in NGINX.Concept high; verify specifics per version/OS
Non-blocking I/OThe reason a worker doesn’t freeze on a slow backend: it registers the FD and moves on, returning when the bell rings. The load-bearing behaviour behind the whole event loop.High
Edge- vs level-triggeredepoll has two notification modes; edge-triggered fires only at the moment state changes. Fairly confident NGINX uses edge-triggered by default — treat as “verify against nginx.org” rather than settled.Medium — flagged
Accept mutex / SO_REUSEPORTWhen 1,000 connections arrive at once, how are they divided among the workers’ boards so one isn’t slammed while others nap? A real coordination fight over “who grabs the new guest at the door.”Real mechanism; worth a focused read
FD limitsFDs are handed out as small integers — and a process can run out. The “open file descriptor limit” (ulimit -n) is a classic way a busy server falls over. Ties straight back to C10k.High

15

Quick-reference glossary

TermIn one lineRestaurant analogy
NGINXFront-door web server / reverse proxyThe maître d’
C10k problemCan one server handle 10,000 concurrent connections?10,000 guests, not enough waiters
Reverse proxySits in front of app servers, forwards requestsHost routing guests to kitchens
Upstream / backendYour actual application serverThe kitchen / chefs
File descriptor (FD)Small integer naming one open connection/fileThe coat-check ticket
select / pollOld O(n) readiness check — scans everythingWalking past every table
epollLinux O(active) readiness mechanism — ready listThe bell-board
epoll instanceThe board itself; an FD holding interest + ready listsOne waiter’s bell-board
kqueueepoll’s equivalent on BSD / macOSA different brand of bell-board
worker processOne event loop, typically one per CPU coreOne waiter per section
non-blocking I/ORegister and move on instead of waitingDon’t stand frozen at one table
.htaccessApache per-directory config, read every requestHouse-rules card taped in a room

16

Recall questions & diffuse-mode seeds

Active recall beats re-reading. Cover the answers and try these cold.

Retrieve it

1. What specifically “ran out” first in the old thread-per-connection model, and what’s the one-sentence trick NGINX uses instead?
2. A developer on cheap shared hosting wants to password-protect one folder with no access to main config — which of Apache/NGINX/HAProxy lets them, and what’s the per-request cost?
3. When a browser connects, what does NGINX hold that represents it? Why don’t 5,000 idle new connections slow the worker — what is epoll not doing with their tickets?
4. In the round trip, between step 4 (request sent) and step 6 (result read), what is the worker doing? (Not standing at FD 51 tapping its foot.)
5. On an 8-core server with worker_processes auto;, roughly how many epoll boards exist — and does a new connection register onto all of them or just one?

Diffuse-mode seeds · chew on these away from the screen

• If NGINX’s superpower is not a worker per connection, what happens when one request asks it to do something genuinely slow and blocking — like reading a giant file off a slow disk? (→ the slow-disk asymmetry, sendfile/aio/thread pools.)
• Each worker has its own board and a connection sticks to one worker — so when 1,000 connections arrive in one second, how do they get divided so one worker isn’t slammed while others nap? (→ accept mutex / SO_REUSEPORT.)
• FDs are small integers handed out low — could a slammed server run out of numbers? (→ the open file-descriptor limit; the other face of C10k.)
• Apache re-reads .htaccess every request; NGINX reads config once per reload. Who would actually prefer the slower “re-read every time” behaviour — and why does that explain why .htaccess still exists?

§

References

A verification trail. Prefer tier-1 sources (the spec, official docs) over blogs.

TopicSource
The C10k problemDan Kegel, The C10K problem — kegel.com/c10k.html
NGINX architecture & historyThe Architecture of Open Source Applications, Vol. II, “nginx” (Andrew Alexeev) — aosabook.org; “Inside NGINX” — nginx.com
NGINX official docsnginx.org/en/docs — event methods, events/use, auth_basic, autoindex, proxy modules
“If Is Evil” / rewrite conversionnginx.org converting rewrite rules
epoll APILinux man pages: epoll(7), epoll_create(2), epoll_ctl(2) — man7.org
select / poll (O(n) behaviour)Linux man pages: select(2), poll(2) — man7.org
File descriptors & “everything is a file”Linux man pages: open(2); foundational Unix design — man7.org
Apache .htaccess & MPMshttpd.apache.org/docs/2.4/howto/htaccess.html; mpm.html
HTTP Basic AuthenticationRFC 7617, The ‘Basic’ HTTP Authentication Scheme — rfc-editor.org
Confidence calibration carried from the lesson: high — C10k concept, NGINX 2004/Sysoev/Rambler, event-driven vs per-connection, epoll being an FD with interest/ready lists, one-epoll-per-worker, FD fundamentals. Verify before relying — exact Apache event-MPM version, FD_SETSIZE (~1024), whether NGINX defaults to edge-triggered epoll, and any specific syntax/flags. No memory figures or benchmarks were asserted; none were reliable.