PHP HTTP server processing to immediately send HTTP response, while continue backend processing in separate process
I’m implementing a PHP application on server A that receives a request from a client (browser), which will lead to a HTTP request to be sent to another server B. The client request is then answered (HTTP response sent).
Some time later, server A receives a call back HTTP request from the server B. Server B expects an (immediate) HTTP response with status code 200. Then, this part ends, while server A has to finalize the data received from server B.
Putting the data on some kind of queue, and periodically run a cron job to look at the queue and work on the data is not suitable here, mainly because the data a) shall be worked on immediately, and b) the events rarely occur (the cron job would mostly run for nothing).
So, I was thinking of using fork() to create a child process. The parent will then respond to server B, while the child process would work on the data.
So here is the code without processing details:
<?php
$pid = pcntl_fork();
switch ($pid) {
case -1:
// unexpected error handled here.
exit();
break;
case 0:
// child will process the data in its own process
//
// Not sure whether we still have an open connection to the "caller"
//
exit();
break;
default :
// parent responds to server, then terminates, closing the connection
ignore_user_abort(TRUE);
ob_start();
header('Connection: close');
header('Content-Length: 0');
http_response_code(200);
ob_end_flush();
flush();
// check if fastcgi_finish_request is callable
if (is_callable('fastcgi_finish_request')) {
fastcgi_finish_request();
}
exit(); // This should finally close the connection
break;
}
?>
Does this look like a suitable solution?
I’m unclear about the state of the network connection in the child process. As per manual (as usual fork()) behaviour, the child inherits all open file descriptors, which includes network connections (if I’m not mistaken).
Possibly “pcntl_unshare()” shall be used to unlink network connections from parent? Unfortunately, the parameters like “CLONE_NEWNET” don’t seem to be described in the manual.
Any thoughts or advice are welcome
Your approach using pcntl_fork() is technically viable but has significant caveats regarding network connection handling. The child process inherits all file descriptors from the parent, including network connections, which can lead to unexpected behavior. A more robust solution exists using fastcgi_finish_request() which is specifically designed for this scenario in FastCGI environments.
Contents
- Evaluating Your pcntl_fork Approach
- Network Connection Challenges
- Better Alternative: fastcgi_finish_request
- Connection Management Best Practices
- Implementation Recommendations
- Conclusion
Evaluating Your pcntl_fork Approach
Your implementation follows the standard pattern for process forking in PHP, but there are several important considerations:
Strengths:
- The basic fork structure is correct, with proper error handling
- You’re using
ignore_user_abort(TRUE)to prevent the parent from being terminated - The
Connection: closeheader andContent-Length: 0approach is appropriate for immediate responses - You’re checking for
fastcgi_finish_request()availability
Critical Issue:
The main problem is that both processes share the same network connection descriptors. As Hacking with PHP explains:
“Both parent and child inherit the same file descriptors which means that you can write to the same file from several processes if you want to. In practice this is undesirable as you will get intermingled lines of text from the different process”
This applies equally to network connections. When the parent process closes the connection to respond to server B, the child process may encounter connection errors or unexpected behavior.
Network Connection Challenges
The shared file descriptor behavior creates several potential issues:
Connection State Conflicts
- When the parent flushes and closes the connection, the child may still have references to the same socket
- This can lead to “connection reset by peer” errors in the child process
- Both processes might attempt to use the same network resources simultaneously
Resource Cleanup Problems
- File descriptors are reference-counted - both processes need to close them properly
- Network buffers and connection state may become inconsistent between processes
- Database connections (if any) become invalid in the child process
As noted in the Stack Overflow discussion:
“It is the way pcntl_fork was designed. Any extension that maintains it’s own file descriptors will then have corrupted descriptors because all children an parents share the same file descriptors.”
Solution: Explicit Connection Management
To mitigate these issues, you should:
<?php
$pid = pcntl_fork();
switch ($pid) {
case -1:
// unexpected error handled here.
exit();
break;
case 0:
// child will process the data in its own process
// IMPORTANT: Close all shared network connections
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// Close any database connections
// Close any other file descriptors that shouldn't be shared
// Now process the data
your_processing_function();
exit();
break;
default :
// parent responds to server, then terminates
ignore_user_abort(TRUE);
ob_start();
header('Connection: close');
header('Content-Length: 0');
http_response_code(200);
ob_end_flush();
flush();
// Use fastcgi_finish_request if available - this is cleaner
if (is_callable('fastcgi_finish_request')) {
fastcgi_finish_request();
}
exit();
break;
}
?>
Better Alternative: fastcgi_finish_request
For your use case, fastcgi_finish_request() is actually the superior solution when running under PHP-FPM. According to the PHP manual:
This function finishes request for current process and makes possible to continue running code in background after client connection has been closed.
Advantages over pcntl_fork:
- No process duplication overhead
- No shared file descriptor issues
- Cleaner resource management
- Specifically designed for this exact use case
- Works seamlessly with PHP-FPM’s process management
Implementation:
<?php
// Send immediate response to server B
ignore_user_abort(TRUE);
ob_start();
header('Connection: close');
header('Content-Length: 0');
http_response_code(200);
ob_end_flush();
flush();
// Detach from the client connection
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// Continue processing in the same process
your_processing_function();
?>
When to Use Each Approach
| Use Case | fastcgi_finish_request | pcntl_fork |
|---|---|---|
| Immediate HTTP response + background processing | ✓ | ✓ |
| Heavy computational tasks that benefit from separate processes | ✗ | ✓ |
| Multiple workers needed for parallel processing | ✗ | ✓ |
| Database connection isolation | ✗ | ✓ |
| Resource efficiency | ✓ | ✗ |
| Code simplicity | ✓ | ✗ |
Connection Management Best Practices
For pcntl_fork Approach
-
Close All Database Connections
php// Before forking $db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass'); // In child process if ($pid == 0) { $db = null; // Close connection // Reconnect if needed $db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass'); } -
Handle File Descriptors
php// In child process fclose(STDIN); fclose(STDOUT); fclose(STDERR); // Or redirect to /dev/null fclose(STDIN); fclose(STDOUT); fclose(STDERR); -
Signal Handling
php// Set up signal handlers in child pcntl_signal(SIGTERM, "signal_handler"); pcntl_signal(SIGINT, "signal_handler"); function signal_handler($signo) { // Clean up and exit exit(); }
For fastcgi_finish_request Approach
-
Process Isolation
- The same process continues execution after client disconnect
- No need for special connection cleanup
- All resources remain available
-
Error Handling
phptry { fastcgi_finish_request(); // Processing code here } catch (Exception $e) { // Error handling - won't affect the client error_log("Processing error: " . $e->getMessage()); }
Implementation Recommendations
Recommended Solution: fastcgi_finish_request
<?php
/**
* Handles callback from server B with immediate response
* and background processing
*/
function handle_server_b_callback($data) {
// Send immediate 200 response
send_immediate_response();
// Process data in background
process_data_background($data);
}
function send_immediate_response() {
ignore_user_abort(TRUE);
ob_start();
header('Connection: close');
header('Content-Length: 0');
http_response_code(200);
ob_end_flush();
flush();
// Detach from client if possible
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
}
function process_data_background($data) {
// Your processing logic here
// This runs after the client has disconnected
// Example: Process and store the data
$result = your_processing_function($data);
// Log completion
error_log("Background processing completed: " . json_encode($result));
// Additional processing steps...
}
// Main execution
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
handle_server_b_callback($data);
} else {
http_response_code(405);
echo 'Method not allowed';
}
?>
Alternative: pcntl_fork with Proper Cleanup
If you must use pcntl_fork (e.g., for heavy parallel processing):
<?php
function handle_server_b_callback($data) {
$pid = pcntl_fork();
if ($pid == -1) {
// Fork failed
send_error_response(500, "Internal server error");
return;
}
if ($pid) {
// Parent process
send_immediate_response();
// Parent can exit or continue with other tasks
exit();
} else {
// Child process
process_data_in_child($data);
exit();
}
}
function send_immediate_response() {
ignore_user_abort(TRUE);
ob_start();
header('Connection: close');
header('Content-Length: 0');
http_response_code(200);
ob_end_flush();
flush();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
}
function process_data_in_child($data) {
// Clean up shared resources
cleanup_shared_resources();
// Process the data
$result = your_processing_function($data);
// Log completion
error_log("Child process completed: " . json_encode($result));
}
function cleanup_shared_resources() {
// Close database connections
// Close file handles
// Clean up any shared state
// Example for MySQL connections
if (function_exists('mysql_close')) {
mysql_close();
}
// Redirect stdio to /dev/null
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$nullFile = fopen('/dev/null', 'r');
if ($nullFile) {
STDIN = $nullFile;
}
$nullFile = fopen('/dev/null', 'w');
if ($nullFile) {
STDOUT = $nullFile;
STDERR = $nullFile;
}
}
?>
Conclusion
Your pcntl_fork() approach is functionally correct but introduces unnecessary complexity and potential connection management issues. The fastcgi_finish_request() function provides a cleaner, more efficient solution for your specific use case of sending an immediate HTTP response while continuing background processing.
Key recommendations:
- Use
fastcgi_finish_request()when running under PHP-FPM - it’s specifically designed for this scenario - Avoid
pcntl_fork()unless you need true process isolation or parallel processing capabilities - Implement proper resource cleanup if you must use forking
- Test thoroughly with connection scenarios to ensure reliability
- Consider error handling in background processes that won’t affect the immediate response
For your use case where you need to immediately respond to server B while continuing data processing, the fastcgi_finish_request() approach is superior in terms of simplicity, resource efficiency, and reliability.
Sources
- PHP: pcntl_fork - Manual
- Duplication of resources when forking – Hacking with PHP
- PHP: fastcgi_finish_request - Manual
- PHP fork process - parent reading variables updated by child - Stack Overflow
- php - pcntl_fork and the MySQL connection is gone - Stack Overflow
- Continue processing after closing connection - Stack Overflow
- Running Long-Running Tasks in PHP: Best Practices and Techniques