Answer First, Work Later: Post-Response Processing in PHP
A checkout endpoint I inherited took 2.4 seconds. The order insert took 40 milliseconds. The other 2.3 seconds were a confirmation email over SMTP, a Slack webhook and a stock-sync API call - none of which the customer needed to wait for. They just needed "Order confirmed".
On PHP-FPM: fastcgi_finish_request()
If you run nginx + FPM (or Apache with FPM, which is most modern hosting), one function flushes the response to the client and closes the connection while your script keeps running:
echo json_encode(['ok' => true, 'order' => $order->number]);
fastcgi_finish_request(); // customer has their answer NOW
// Everything below happens after the response - user is gone
sendConfirmationEmail($order); // 1.8s - nobody waits
notifySlack($order); // 300ms
syncInventory($order); // 200ms
The browser sees a 40ms response. Your logs see the full runtime. Everyone wins.
Things to know once the request is finished: you can't echo anything anymore, the session should be closed first (you're holding the lock otherwise), and set_time_limit generously because the work now runs unattended. Wrap the after-work in try/catch and log failures - there's no user left to show errors to.
On mod_php: the best-effort version
Apache with mod_php (classic XAMPP included) has no exact equivalent, but you can get close for small payloads:
ignore_user_abort(true); // keep running if the client disconnects
ob_start();
echo $json;
header('Content-Length: ' . ob_get_length());
header('Connection: close');
ob_end_flush();
flush(); // push it out the door
// Browser *usually* releases now; slow work follows
sendConfirmationEmail($order);
The Content-Length + Connection: close combo tells the browser the response is complete so it stops spinning. It's less bulletproof than FPM (gzip/output buffering at the server layer can interfere), so test on your actual stack.
register_shutdown_function: the portable hook
register_shutdown_function(function () use ($order) {
writeAuditLog($order);
});
Shutdown functions run after script execution on every runtime - and on FPM, after fastcgi_finish_request they run post-response automatically. They're also the classic place to catch fatal errors for logging (error_get_last() inside a shutdown function sees them).
Know when to graduate to a real queue
This pattern is "poor man's async" - brilliant for shaving seconds off checkout with zero infrastructure, wrong for anything needing retries, ordering or visibility. When the after-work starts failing silently or takes 30+ seconds, that's your cue for a proper job queue (database-driven is fine). Until then, one function call buys you the UX of async with none of the moving parts.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.