15.1 The Extension Landscape
You need a thing NGINX doesn’t do. Before reaching for any tool, know the four you’re choosing between — each buys a different kind of power and bills a different kind of cost.
Advanced
What you will learn
The problem-shape taxonomy: which need implies which tool.
The four mechanisms, scored honestly.
The decision flowchart, with the maintenance bill in the loop.
1. Problem shapes, not solutions
Extension requests arrive in four shapes, and the shape — not the buzzword — picks the tool:
You need to... | Shape |
|---|---|
run a protocol/codec the binary lacks (brotli, geoip2, pagespeed, modsecurity) | a module |
compute, parse, or decide per-request (JWT claims, exotic header munging, dynamic keys) | scripting |
change what an existing directive DOES (a patch: different balancing math, a custom log field the C code must emit) | source modification |
run a whole app-tier ecosystem inside nginx (Lua libraries, complex gateways) | a platform fork |
Diagnose the shape first. Most "we need OpenResty" conversations end at "we needed
js_set"; most "we should patch nginx" conversations end at "a map would do."
2. The four mechanisms
A. Dynamic modules (this module's Lesson 15.3) — a
.so loaded at startup with
load_module:
load_module modules/ngx_http_brotli_filter_module.so;Buys: the packaged binary stays your binary — same security updates, same ops, same muscle memory. The module is the only new thing on disk.
Bills: binary compatibility (the module must match the binary's build signature — Lesson 15.3's war story), version coupling on every nginx upgrade, and the supply chain of whoever built that
.so(yours to own, if you build from source as in Lesson 15.2).
B. njs (Lesson 15.4) — sandboxed JavaScript inside the worker:
js_set $auth_sub app.authSub; # computed per requestBuys: computation without compilation; deploys with your config; impossible to segfault the worker from JS (by design — no FFI, no filesystem, no network).
Bills: the sandbox's edges are real (no
require, no arbitrary syscalls — Lesson 15.4 probes them); per-request JS costs CPU (nanoseconds-to-microseconds — negligible until it isn't); a second language in your config repo.
C. Source patches / custom builds (Lesson 15.2) — fork the tarball, patch the C, build:
Buys: literally anything — you own the machine.
Bills: you own the machine. Every CVE rebase carries your patch forward; every distro security update is now yours to reproduce. This is the option that must justify itself with a business case, not a Tuesday.
D. OpenResty / platforms — nginx-plus-LuaVM distributions:
Buys: full application-tier power inside nginx (cosockets, shared dicts, entire gateways in Lua); a mature ecosystem (Kong built on it).
Bills: you're no longer running "nginx" — you're running a distribution with its own release cadence, its own module set, and its own debugging reality. The skills from this series transfer (it IS nginx underneath); the operational responsibility expands.
3. The decision flowchart
Need it?
├─ config can express it (map/variables/rewrite)? -> DONE. (Modules 2, 11)
├─ it's a known published module? -> dynamic module (15.3)
├─ it's per-request computation? -> njs (15.4)
├─ it's an app ecosystem, not a feature? -> OpenResty
└─ it requires changing C behavior? -> patch, reluctantly (15.2)Two meta-rules keep the flowchart honest:
The config tier is the cheapest tier. The series spent fourteen modules proving how far maps, variables, phases, and the standard modules go. Every step past config is a step of permanent maintenance — take it only when config is provably exhausted (and "provably" includes asking someone who's read Module 11).
The maintenance bill compounds. One dynamic module: a line in your upgrade runbook. Four modules plus a patch plus custom builds: you're now a distribution vendor. Count the bills before signing.
Try it yourself
The example setup in this module runs the "one module" tier: njs arrived as
.deb-extracted.sofiles (extracted from nginx.org’s own packages — a supply chain you can name), brotli as your own build. Compare the two supply chains — sameload_moduleline, very different provenance stories.Audit yourself: pick any three needs from earlier modules (WAF rules, auth claims, compression) and name which tier satisfied each. Notice how often it was tier zero (config).
Q&A
Q: Where does ModSecurity fit? A: As a dynamic module (libmodsecurity + the connector module) — tier A of the flowchart, with a heavier runtime footprint than most. The WAF you built in Module 7 was tier zero; the moment rule sets outgrew maps, you'd step up.
Q: Is njs "slow nginx"? A: No — it's compiled bytecode in the worker, no process spawns, microsecond-scale for typical request munging. It's slower than C for hot loops and faster than anything involving a subrequest round trip. The sandbox, not the speed, is the design center.
Q: Why not just compile modules INTO the binary? A: Static linking is legitimate (distros do it) — but it makes every module change a full binary rebuild and makes the binary nonstandard. Dynamic modules decouple: same binary, different
.so sets per edge role.
Takeaways
Diagnose the problem shape first: module, computation, behavior change, or ecosystem.
Four mechanisms, four bills: dynamic modules (compat + upgrades), njs (sandbox edges), patches (you're the vendor), platforms (you're running a distribution).
The config tier is the cheapest tier — Modules 2 and 11 made it surprisingly deep; step past it provably, not habitually.
One module is a runbook line; five plus patches is a distribution business. Count the compounding bills before you sign.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.