Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~40 min read

Intermediate

What you will learn

  1. Why FastCGI exists at all (and why it still runs a third of the web).

  2. fastcgi_pass + fastcgi_param — the complete parameter model, with a PHP instrument that shows exactly what arrives.

  3. uwsgi_pass — same idea, different dialect; this lesson runs a real uWSGI server on the uwsgi wire protocol.

  4. SCGI in one paragraph (it's the minimal common denominator).

  5. The operational differences that matter: buffering, SCRIPT_FILENAME disease, 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:

text
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):

nginx
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:

nginx
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 channel

The PHP instrument (a one-file index.php that prints back its $_SERVER) — here is the real, complete capture through the gateway:

bash
$ 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:

  1. The app sees only what you parameterized. HTTP_HOST arrived because we sent it. x_real_ip arrived because we explicitly added HTTP_X_REAL_IP. Unlike proxy_pass (a whole HTTP request crosses), FastCGI hands the app a key/value environment — but nginx auto-converts every client request header into HTTP_* 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.

  2. SCRIPT_FILENAME is 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's root mapping (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.

  3. App configuration rides the same channel. APP_ENV reached getenv() — the pattern scaled up is fastcgi_param-based feature flags and env-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):

bash
uwsgi --socket 127.0.0.1:9021 --wsgi-file wsgi.py --processes 2
nginx
location /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:

bash
$ 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_INFO is the WSGI routing variable — Django/Flask match URLs against it. The standard mapping for a root-mounted app is uwsgi_param PATH_INFO $document_uri; (our example does exactly this); mounted-under-prefix apps get SCRIPT_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

proxy_pass (HTTP)

fastcgi_pass / uwsgi_pass

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 ( fastcgi_read_timeout…)

Errors from the app

status codes + proxy_intercept_errors

status codes + fastcgi_intercept_errors

The classic failure

Host/URI confusion (Lesson 4.1)

SCRIPT_FILENAME/ PATH_INFO pointing nowhere

Pool management

Module 5's upstream

Module 5's upstream — pools work identically here

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

  1. Read the PHP instrument (§2's capture) — then interrogate the automatic ride: set fastcgi_pass_request_headers off;, reload, and watch "host" become null in the response. One switch, every client-header param gone — that they were ever there is the lesson: headers ride in by default; explicit fastcgi_param lines override or extend.

  2. Break SCRIPT_FILENAME on purpose. Change the location's root to www/main (where no PHP lives), reload, request /index.php, read PHP-FPM's complaint in the response and the NGINX error log. Restore. Now add try_files $uri =404; and request /nonexistent.php — meet the safe 404 instead.

  3. Teach the WSGI app a new param. Add uwsgi_param APP_ENV example;, reload, and find it in environ_keys.

  4. Timing semantics. The PHP app ships a slow.php — request /slow.php and compare against the default fastcgi_read_timeout (60s): it succeeds. Now set fastcgi_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_FILENAME is the load-bearing FastCGI parameter — and try_files $uri =404; is its bodyguard.

  • uwsgi_param mirrors fastcgi_param one-for-one; PATH_INFO is 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's upstream works for all of them.

  • Modern defaults: FastCGI for PHP, HTTP/gRPC for the rest, SCGI for that one embedded box you'll inherit someday.

Was this page helpful?

Mark this learning resource complete to track your learning progress.