skip to content

Under PHP-FPM, what does fastcgi_finish_request() do, and what are its pitfalls when used for background work?

level: seniorimportance: nice to knowfreq 25%

answer

  1. flush and close, keep running
  2. returns true, or false if closed
  3. worker stays busy
  4. session lock still held
  5. FPM-only function

basics

~20 s

fastcgi_finish_request() sends all buffered output to the client and ends the HTTP response, while the PHP script keeps running. The worker stays busy, locks such as the session remain held, later output is lost, and the function exists only under PHP-FPM.

solid answer

~50 s

`fastcgi_finish_request(): bool` flushes all output buffers and headers, ends the FastCGI request so the web server completes the HTTP response, and returns `true`; the script then continues, for example to send an e-mail or write statistics. It returns `false` if the request was already finished. Pitfalls: the **worker stays occupied** until the script ends, so slow post-response work eats into `pm.max_children`, and `request_terminate_timeout` stops applying after the call unless `request_terminate_timeout_track_finished = yes`; anything printed afterwards no longer reaches the client; a started session keeps its lock until the script ends, so call `session_write_close()` before the slow part or the user's next request blocks; failures after the response cannot be reported to the user. The function is defined only by the FPM SAPI, so CLI and other SAPIs need a `function_exists()` check. For heavy or must-succeed work, a job queue is the better tool.

code

php · 17 lines
php
<?php
declare(strict_types=1);

function respondThen(callable $afterResponse): void
{
    if (session_status() === PHP_SESSION_ACTIVE) {
        session_write_close(); // do not keep the session locked
    }
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request(); // PHP-FPM only
    }
    try {
        $afterResponse();
    } catch (Throwable $e) {
        error_log('post-response task failed: ' . $e->getMessage());
    }
}

go deeper

for a junior

Recall that fastcgi_finish_request() sends the response to the user while the PHP script keeps running afterwards.

for a middle

Explain its return value, why output after it is lost, the session lock, and why it needs a function_exists() guard outside PHP-FPM.

for a senior

Judge when post-response work is acceptable: worker occupancy against pm.max_children, lost failures, and when a job queue is the right design.

for a principal

Set the rule for background work across services: what may run after the response and what must go through queues with retries and monitoring.

## What it does `fastcgi_finish_request()` is a function provided by the **PHP-FPM SAPI** (it is declared in FPM's own stub, not in a general extension). Its signature is: ```php fastcgi_finish_request(): bool ``` When called, it: 1. flushes every active output buffer and sends the HTTP headers; 2. ends the FastCGI request, so the web server can finish the HTTP response and the browser stops waiting; 3. returns `true` — or `false` if the request had already been finished. The PHP script **does not stop**. Code after the call runs as usual, invisibly to the user. The PHP manual describes the use case as time-consuming tasks performed "without leaving the connection to the client open" — video conversion or statistics processing in its examples. ```php <?php declare(strict_types=1); echo json_encode(['status' => 'accepted']); session_write_close(); // release the session lock first fastcgi_finish_request(); // the client gets its response now $mailer->sendReceipt($order); // runs after the response $stats->record($order); ``` ## Pitfall 1: the worker is still busy A PHP-FPM worker handles one request at a time, and it stays **busy** until the script ends — the response being finished changes nothing for FPM's accounting. If post-response work takes 5 seconds, every such request holds a worker for 5 seconds; under load that uses up `pm.max_children` just as a slow page would, and new requests queue. Worse, the pool's `request_terminate_timeout` is **not** engaged after `fastcgi_finish_request()` unless `request_terminate_timeout_track_finished = yes` is set, so a hung post-response task can hold a worker far longer than any page could. ## Pitfall 2: locks outlive the response - A **session** started with `session_start()` keeps its file lock until the script ends or `session_write_close()` is called. The user's next request, blocked on the same session, hangs — even though the previous page "finished". Close the session before calling `fastcgi_finish_request()`. - Database transactions and file locks held by the script are held for the extra time too. ## Pitfall 3: output and errors go nowhere After the call the client is gone: - `echo`, `header()` and `http_response_code()` no longer reach the browser; the response is already complete. - If the background part fails, the user saw a success message anyway. Errors must be logged, and anything that must succeed needs retries — which the request has no mechanism for. ## Pitfall 4: portability Because the function exists only under PHP-FPM, code that also runs under the CLI (tests, queue workers) or another SAPI must guard it: ```php if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } ``` Some other SAPIs offer their own equivalent functions, but they are separate APIs. ## The capacity arithmetic A pool's throughput is roughly workers divided by the time each request holds a worker. With 20 workers and pages that take 100 ms, the pool can serve about 200 requests per second. Add 400 ms of post-response work to every request and each worker is held for 500 ms: capacity falls to about 40 requests per second, although every user still sees a 100 ms response. That hidden cost is why post-response work must stay short. ## When to use it and when not | Situation | Suitable? | |---|---| | A few hundred milliseconds of logging or cache warming | Yes | | Sending one notification where loss is tolerable | Usually | | Seconds of work on every request | No — it drains the worker pool | | Work that must succeed or be retried | No — use a job queue | | Long jobs (exports, media processing) | No — a queue and a CLI worker | A **job queue** (a database table, an in-memory list or a message broker consumed by CLI workers) moves the work out of the FPM pool entirely: web workers return immediately, the job runs with its own concurrency limit, and failures can be retried and monitored. `fastcgi_finish_request()` is the lightweight option for small, best-effort tasks. ## Related behaviour - **`register_shutdown_function()`** callbacks still run at the end of the script, after the post-response work. - **No arguments.** The function takes no parameters; passing one throws an `ArgumentCountError`. - **Calling it twice** is harmless: the second call returns `false` because the request is already finished.

  • After adding fastcgi_finish_request(), the page feels faster but the server hits pm.max_children more often. Why?
    Users get their response earlier, but each PHP-FPM worker is still busy until the post-response work ends. If that work is slow, workers are held longer than before, so the pool runs out of free workers sooner. Move slow tasks to a queue consumed by CLI workers.
  • Why can a user's next page hang after a request that called fastcgi_finish_request()?
    If that request had an open session, PHP's session lock is held until the script ends or `session_write_close()` runs. The next request with the same session ID blocks in `session_start()` until the background work finishes. Close the session before finishing the response.

saying these in an interview costs you the question

  • fastcgi_finish_request() frees the worker for the next request
  • The script stops running after fastcgi_finish_request()
  • Output after the call is sent in a second response
  • fastcgi_finish_request() is available in every SAPI, including the CLI
  • It is a replacement for a job queue