4.3 Speaking Application: FastCGI, uWSGI, SCGI
The gateway pattern predates HTTP-everywhere: purpose-built wire protocols between web servers and app runtimes. NGINX speaks them natively — and the differences from proxy_pass are the lesson.
Intermediate
What you will learn
Why FastCGI exists at all (and why it still runs a third of the web).
fastcgi_pass+fastcgi_param— the complete parameter model, with a PHP instrument that shows exactly what arrives.uwsgi_pass— same idea, different dialect; this lesson runs a real uWSGI server on the uwsgi wire protocol.SCGI in one paragraph (it's the minimal common denominator).
The operational differences that matter: buffering,
SCRIPT_FILENAMEdisease,PATH_INFO, and pooling.
1. Why these protocols exist
proxy_pass speaks HTTP to the backend. But in the CGI era, running an app meant: fork a process per request, hand it environment variables on stdin, read stdout back. FastCGI (1996) fixed the obvious waste:
keep the app process alive and speak a compact binary protocol to it over a socket — no per-request fork, no HTTP parsing in the app, and a
REMOTE_ADDR-style environment that Perl scripts of the time already understood.
Two descendants matter today:
FastCGI — PHP's native transport. PHP-FPM ("FastCGI Process Manager") is a pool of PHP workers listening on a FastCGI socket. This is how a huge share of the web's PHP actually runs.
uWSGI protocol — the Python world's equivalent from the uWSGI project: a compact 4-byte-header protocol that also carries a modified-CGI environment to WSGI applications.
Both share the same architecture from NGINX's side of the socket:
client ──HTTP──▶ NGINX ──FastCGI/uWSGI wire──▶ app pool (persistent workers)NGINX translates HTTP ⇄ the app dialect, exactly like
proxy_pass translates HTTP ⇄ HTTP — but with a crucial difference in what the app
receives:
not headers, but a key/value environment you must build explicitly.
2.
fastcgi_pass and the parameter model
The PHP location (working verbatim):
location ~ \.php$ {
root www/php; # where the .php files live
include fastcgi_params; # the parameter set (ours, in conf/)
fastcgi_pass 127.0.0.1:9020; # PHP-FPM's pool socket
}And our
fastcgi_params file — deliberately explicit, annotated:
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param HTTP_HOST $http_host;
# Client headers DO cross automatically as HTTP_* params
# (fastcgi_pass_request_headers, default on). The explicit lines below
# override or add on top of the auto-set:
fastcgi_param HTTP_X_REAL_IP $remote_addr;
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
fastcgi_param APP_ENV example; # app config via the same channelThe PHP instrument (a one-file
index.php that prints back its
$_SERVER) — here is the real, complete capture through the gateway:
$ curl -s -H 'Host: gw.example.test' http://127.0.0.1:8400/index.php
{
"server": "b-php-fpm",
"php": "8.4.24",
"sapi": "fpm-fcgi",
"method": "GET",
"uri": "/index.php",
"script_name": "/index.php",
"script_filename": "/var/www/php/index.php",
"document_root": "/var/www/php",
"host": "gw.example.test",
"remote_addr": "127.0.0.1",
"x_real_ip": "127.0.0.1",
"x_forwarded_for": "127.0.0.1",
"query_string": "",
"app_env": "example",
"get": []
}Read the lesson out of the output:
The app sees only what you parameterized.
HTTP_HOSTarrived because we sent it.x_real_iparrived because we explicitly addedHTTP_X_REAL_IP. Unlikeproxy_pass(a whole HTTP request crosses), FastCGI hands the app a key/value environment — but nginx auto-converts every client request header intoHTTP_*params (fastcgi_pass_request_headers, on by default). What you must spell out are the non-header CGI values:QUERY_STRING,SCRIPT_FILENAME,CONTENT_TYPE,CONTENT_LENGTH,REMOTE_ADDR, and app-specific entries.SCRIPT_FILENAMEis the load-bearing parameter. It tells PHP-FPM which file to execute — and it's built from$document_root$fastcgi_script_name, i.e. it depends on the location'srootmapping (Lesson 3.1). The legendary "Access denied" / "File not found" / blank-page triad of PHP misconfigurations is almost always this one value pointing at the wrong path.App configuration rides the same channel.
APP_ENVreachedgetenv()— the pattern scaled up isfastcgi_param-based feature flags andenv-style injection, per vhost, without touching the app.
3.
uwsgi_pass — the same idea, Python's dialect
The example runs a
real uWSGI server on the uwsgi
wire protocol (the distinction matters: uWSGI can also speak HTTP, but then
uwsgi_pass would 502 — NGINX speaks the binary dialect):
uwsgi --socket 127.0.0.1:9021 --wsgi-file wsgi.py --processes 2location /wsgi/ {
include uwsgi_params; # same shape as fastcgi_params
uwsgi_pass 127.0.0.1:9021;
}The environment transmit is one-for-one with FastCGI (
uwsgi_param ↔
fastcgi_param), which the example WSGI app proves by printing its environ:
$ curl -s -H 'Host: gw.example.test' http://127.0.0.1:8400/wsgi/x | jq -r .environ_keys[0:8]
['CONTENT_LENGTH', 'CONTENT_TYPE', 'DOCUMENT_ROOT', 'DOCUMENT_URI',
'HTTP_ACCEPT', 'HTTP_HOST', 'HTTP_USER_AGENT', 'PATH_INFO', ...]Every
uwsgi_param line landed as a WSGI environ entry. The two protocol-specific notes worth keeping:
PATH_INFOis the WSGI routing variable — Django/Flask match URLs against it. The standard mapping for a root-mounted app isuwsgi_param PATH_INFO $document_uri;(our example does exactly this); mounted-under-prefix apps getSCRIPT_NAME+ the remainder.Sockets vs HTTP: production uWSGI almost always exposes a socket (unix or TCP) exclusively to NGINX — never a public HTTP listener. Same ops posture as PHP-FPM: the app pool is private infrastructure.
4. SCGI in one paragraph
scgi_pass +
scgi_param — the minimalist "Simple CGI" dialect (an informal spec, never RFC-ified): the same environment model, a text-framed protocol, zero exotic features. You will meet it in embedded devices and the occasional C-based app; everything you know from FastCGI transfers unchanged (including
SCRIPT_FILENAME's analog). NGINX ships it for completeness; modern deployments overwhelmingly choose FastCGI (PHP) or HTTP/gRPC (everything else).
5. The operational differences from
proxy_pass
Concern |
|
|
|---|---|---|
What the app receives | HTTP request, client headers flow by default | key/value environment; explicit params only |
Default upstream protocol | HTTP/1.0 (upgradable to 1.1) | the app dialect, persistent connections |
Body handling | streamed/buffered per Lesson 4.6 | same machinery, same directives (
|
Errors from the app | status codes +
| status codes +
|
The classic failure | Host/URI confusion (Lesson 4.1) |
|
Pool management | Module 5's
| Module 5's
|
That last row matters:
fastcgi_pass http://php-pool; with
upstream php-pool { server 127.0.0.1:9020; server 127.0.0.1:9022; } is completely legal — Module 5's whole curriculum applies to application servers, not just HTTP backends.
Try it yourself
Read the PHP instrument (§2's capture) — then interrogate the automatic ride: set
fastcgi_pass_request_headers off;, reload, and watch"host"becomenullin the response. One switch, every client-header param gone — that they were ever there is the lesson: headers ride in by default; explicitfastcgi_paramlines override or extend.Break
SCRIPT_FILENAMEon purpose. Change the location'sroottowww/main(where no PHP lives), reload, request/index.php, read PHP-FPM's complaint in the response and the NGINX error log. Restore. Now addtry_files $uri =404;and request/nonexistent.php— meet the safe 404 instead.Teach the WSGI app a new param. Add
uwsgi_param APP_ENV example;, reload, and find it inenviron_keys.Timing semantics. The PHP app ships a
slow.php— request/slow.phpand compare against the defaultfastcgi_read_timeout(60s): it succeeds. Now setfastcgi_read_timeout 1s;in the location and watch it 504 — Lesson 4.6's timeout machinery, identical in dialect.
Q&A
Q1. Why does
fastcgi_pass_request_headers off; blank
HTTP_HOST in the PHP app's environment, when HTTP backends receive Host automatically?
Answer
FastCGI isn't HTTP — the app receives a key/value environment. By default nginx converts every client request header into that environment as
HTTP_* params (the CGI convention PHP maps into
$_SERVER);
fastcgi_pass_request_headers off removes the ride, and only explicit
fastcgi_param lines cross after that.
Q2. "No input file specified." from PHP-FPM. What's broken?
Answer
SCRIPT_FILENAME points somewhere PHP-FPM can't open (wrong
root, missing file, or FPM's chdir restrictions). Print the value via the PHP instrument above, fix the
root/mapping (Lesson 3.1), and add
try_files $uri =404; to fail fast on missing files.
Q3. You need two PHP pools (vhost A on 9020, vhost B on 9022) with different
APP_ENV env values. Sketch it.
Answer
Two server blocks (or one with a
map on
$host choosing
$pool), each with its own
fastcgi_pass and an extra
fastcgi_param APP_ENV … line. Params inherit/cancel like any directive — the Lesson 2.5 law still applies.
Q4. Why does pointing
uwsgi_pass at a uWSGI started with
--http-socket produce 502?
Answer
uwsgi_pass speaks the binary uwsgi wire protocol;
--http-socket listens for HTTP. Two dialects, one port, zero mutual intelligibility — a classic misconfiguration, and the fix is
--socket (uwsgi protocol).
Takeaways
FastCGI/uWSGI keep app processes alive behind a purpose-built dialect; NGINX translates HTTP ⇄ environment, and nothing crosses that you don't parameterize.
SCRIPT_FILENAMEis the load-bearing FastCGI parameter — andtry_files $uri =404;is its bodyguard.uwsgi_parammirrorsfastcgi_paramone-for-one;PATH_INFOis the WSGI router's truth.The app pool is private infrastructure (sockets, not public listeners); pooling, timeouts, and interception behave identically to
proxy_pass— and Module 5'supstreamworks for all of them.Modern defaults: FastCGI for PHP, HTTP/gRPC for the rest, SCGI for that one embedded box you'll inherit someday.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.