Request IDs for Nginx and PHP - Follow One Request Through the Logs
A PHP request fails, but the visible clues are scattered. Nginx records a status and duration in one file. PHP records an exception somewhere else. Several requests may have reached the same route within the same second. Which application event belongs to the access-log line under investigation?
A request ID offers a deliberately small answer: create one opaque identifier at the web-server boundary, carry it into PHP, and include it in every relevant record for that request. It does not explain the failure by itself. It makes the evidence easier to join.
This article builds that narrow pattern for Nginx and PHP-FPM. The example avoids treating a client-supplied header as authoritative, keeps personal data out of the identifier, and stops short of calling a single ID “distributed tracing.”
The Problem Is Correlation, Not a Lack of Logs
Web-server and application logs answer different questions. An Nginx access log can show when a request completed, its HTTP status, and how long processing took. Application code knows which operation failed and why. According to the OWASP Logging Cheat Sheet, application logging adds context that infrastructure logging alone often lacks.
Time, path, and client address can help match the two, but they are weak join keys. Concurrent requests can share those values. A request ID provides a more direct link:
Nginx access log:
request_id=91f... status=500 request_time=0.184
PHP application log:
request_id=91f... event=profile_update_failed
The useful property is consistency. The same value appears at each observation point for one request. The ID should remain an identifier, not become a container for a username, email address, IP address, timestamp, or error description.
Give the ID One Trusted Origin
Nginx exposes a built-in $request_id variable. Its core-module documentation describes the value as 16 random bytes represented in hexadecimal. That gives this small stack a convenient server-generated value before PHP handles the request.
The trust boundary matters. A public client can send a header named X-Request-ID, but accepting it unchanged permits repeated, misleading, or malformed values to enter the logs. There are valid architectures in which a trusted reverse proxy propagates an existing correlation or trace context. That requires an explicit trust policy. The simpler baseline here is different: Nginx creates the authoritative local ID and passes it to PHP under a dedicated FastCGI parameter.
The resulting contract is short:
- Nginx generates the value.
- Nginx writes it to the access log.
- Nginx passes the same value to PHP-FPM.
- Application log entries include it without changing it.
- The value is never used for authentication, authorization, or idempotency.
Put the Request ID in the Nginx Access Log
The Nginx log module allows a named log_format to contain variables. It also supports escape=json, which escapes characters that cannot safely appear unescaped in JSON strings.
A compact format can be declared in the http context:
log_format correlated escape=json
'{"time":"$time_iso8601",'
'"request_id":"$request_id",'
'"method":"$request_method",'
'"uri":"$uri",'
'"status":$status,'
'"request_time":$request_time}';
access_log /var/log/nginx/app-access.log correlated;
This format intentionally does not record the query string. Query parameters sometimes contain tokens, search terms, email addresses, or other data that does not belong in a routine log. Omitting them is not a complete privacy policy, but it is a useful data-minimization default. The URI itself and other metadata can still be sensitive, so log access and retention remain separate decisions.
JSON escaping is also not a magic security layer. It protects the structure of this output format, while OWASP separately recommends sanitizing event data from other trust zones and excluding secrets and sensitive identifiers. A request ID cannot compensate for logging passwords or access tokens.
Pass the Server-Generated Value to PHP
Inside the PHP location, pass the value as an explicit FastCGI parameter:
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param REQUEST_ID $request_id;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
The socket path is only a placeholder: Debian installations commonly include a PHP version in that filename, and other systems may use TCP or another path. It must match the actual PHP-FPM pool. The important line is fastcgi_param REQUEST_ID $request_id;. The FastCGI module documentation states that the directive can pass a parameter whose value contains Nginx variables.
PHP normally exposes server and execution-environment values through $_SERVER, although the PHP manual cautions that available entries depend on the web server. With the Nginx configuration above, application bootstrap code can capture the value once:
<?php
$requestId = $_SERVER['REQUEST_ID'] ?? 'missing';
error_log(sprintf(
'[request_id=%s] event=profile_update_failed',
$requestId
));
The error_log() manual notes that the destination depends on PHP and the selected message type. A real application may already have a logger with structured context. If so, add request_id to that context rather than scattering direct error_log() calls through business code.
The fallback value is diagnostic, not a replacement ID. Seeing missing tells an operator that the request did not pass through the expected configuration or that the application was invoked under another SAPI, such as the CLI. Generating a second unrelated ID in PHP would hide that configuration gap and break correlation with Nginx.
Returning the ID Can Help Support
If readers or API clients need to report an identifier, Nginx can return the same value in a response header:
add_header X-Request-ID $request_id always;
The Nginx headers-module documentation says add_header accepts variables, while always adds the field regardless of the response status. This is useful when the interesting response is an error.
X-Request-ID is a local convention in this example, not a standard security header. Returning it is optional. If it is exposed, support tools should treat it as a search key rather than proof that a report is genuine. Anyone who receives a response can know its ID, and the value must not unlock data or actions.
Verify the Whole Path, Not Just the Syntax
A careful rollout has two levels. First, validate the Nginx configuration with the command and service procedure appropriate to the system before reloading it. A copied socket path or a directive in the wrong context should fail here rather than during an incident.
Second, send one controlled request to a harmless test route and compare three places:
- The response header, if it is enabled.
- The Nginx access-log entry.
- A deliberate application-log event from the same request.
All three should contain the same value. Then test an application error path that is safe to trigger. Finally, test what happens when the logging destination is unavailable or full. OWASP explicitly recommends checking logging failures, permissions, injection resistance, and resource exhaustion. Correlation is not reliable if one side silently stops recording.
This check should not print secrets or dump complete request bodies merely to prove that logging works. A fixed event name and the request ID are enough for the correlation test.
Know What This Pattern Does Not Provide
It does not put the ID everywhere automatically
The configuration joins the Nginx access log and the PHP events that explicitly include REQUEST_ID. It does not automatically add the value to every Nginx error message, PHP warning, database query, queue job, or outbound HTTP request. Each additional boundary needs deliberate context handling.
It is not distributed tracing
The W3C Trace Context Recommendation defines traceparent and tracestate for propagating trace relationships across components. A traceparent value contains a trace ID, parent ID, flags, and a version. That model can represent a chain of operations and parent-child relationships. One Nginx request ID copied into PHP cannot.
For a small monolith, that limitation may be acceptable. If one browser action fans out to several services, queues, and background workers, adopting a standard tracing system is usually more coherent than inventing more semantics for X-Request-ID. The local ID can still be useful, but its role should remain explicit.
It does not make logs trustworthy or harmless
An identifier helps connect records; it does not prove that every record is complete, untampered, or correctly attributed. Logs need restricted access, rotation, retention rules, monitoring, and protection against modification. They also need disciplined content. OWASP advises against directly logging passwords, access tokens, session identifiers, connection strings, encryption keys, and sensitive personal data.
There is a quieter privacy tradeoff too: correlation is useful precisely because it links events. Keep the identifier opaque, avoid deriving it from user information, and retain correlated records only as long as their operational or security purpose requires.
A Small Link Between Two Useful Views
A request ID works best when it stays boring. Nginx generates an opaque value, records it with request metadata, and passes it to PHP. The application adds that value to meaningful events. Operators can then move from “a 500 happened around this time” to “these records describe the same request.”
That is a modest improvement, not an observability platform. It will not replace careful event design, safe logging, monitoring, or distributed tracing when a system grows across process boundaries. For a small Nginx and PHP application, however, one trusted identifier can turn two separate logs into a much more useful conversation.
