Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
If a PHP request under IIS launches Chrome with --dump-dom and redirects its output to a file, the likeliest explanation for an empty file and a stalled request is that PHP is still waiting for Chrome or its shell process to finish. An IIS restart can terminate that process chain and make pending output appear; it does not establish that IIS needed to flush the file. Capture Chrome’s stdout, stderr, and exit status directly, impose time limits, and test the command under the IIS application-pool identity before changing buffering settings.
What is happening when the file appears only after an IIS restart?
On Windows, PHP’s exec() starts cmd.exe to launch the requested command. If the command redirects Chrome’s stdout to page.html, the shell owns that redirection while it waits for the child process. PHP, in turn, waits for the command to complete. If Chrome does not exit, the request can remain open and the output file may remain incomplete or appear empty. Restarting IIS can end the request and associated processes, allowing buffered or still-open output to become visible. That is a consequence of process termination, not proof that routine IIS restarts are needed to flush output.
This is the best-supported explanation for the reported symptom, but it is not proof of one universal IIS defect. The archived incident behind this pattern concerned Windows Server 2016 and IIS 10 in 2019; PHP, Chrome, IIS, the target page, and the application-pool identity can all change the behavior. Treat the process chain—W3WP.exe → PHP.exe → CMD.exe → Chrome.exe—as a diagnostic model, not a guarantee that every installation has precisely those processes.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Why Chrome may not finish writing
Chrome’s --dump-dom flag writes the serialized DOM to stdout. It does more than download and print the original HTML: Chrome parses the page, executes scripts that can alter the DOM, then serializes the result. A slow or stalled navigation, long-running page script, blocked network resource, locked profile, or failure specific to the non-interactive IIS environment can therefore delay the output.
#1 Best Overall
Chrome’s --timeout=<milliseconds> flag can bound its wait before capture, but it is not a substitute for a PHP-side deadline. A child can fail in a way that does not yield the output or exit status you expect, and an HTTP request should not be left waiting indefinitely.
Check the installed Chrome version before applying older command-line recipes. Chrome Headless was unified with Chrome in version 112. From Chrome 132, the old in-binary Headless implementation was removed; legacy behavior requires the separate chrome-headless-shell binary. The right binary and supported flags depend on the version actually installed on the server.
Diagnose the process instead of guessing at buffering
- Stop relying on shell redirection for the first test. Use PHP
proc_open()to capture stdout and stderr separately, then record whether the child exits. PHP documents thatexec()startscmd.exeon Windows and thatproc_open()withbypass_shellcan start an external program without that shell boundary. - Record what the request actually ran. Log the Chrome executable path, arguments with secrets removed, elapsed time, stderr, exit code, and whether Chrome processes remain after the request. Do not log API keys, authorization headers, or other credentials.
- Compare execution contexts. Test the exact Chrome executable and URL using a scheduled task or another service-context diagnostic running as the IIS application-pool identity. Keep the working directory, environment, PATH, profile location, output path, and network destination as close as possible to the web request. A successful interactive command-line run does not prove that the IIS identity can do the same work.
- Verify access and isolation. Confirm that the application-pool identity can read the executable and write to the profile, temporary, and output directories. Give each concurrent job a writable, unique
--user-data-dir; sharing Chrome’s default profile can create locks and interference between requests. - Inspect FastCGI limits after the child is observable. Review
requestTimeout,activityTimeout,idleTimeout,maxInstances, and process-recycling behavior. Raising a limit can give a legitimate capture more time, but cannot make a non-terminating Chrome process exit or clean up an orphan. - Check PHP and response buffering last. PHP output buffering,
ob_*calls, FastCGI buffering, or a client/proxy can delay text sent to the browser. They do not explain a child-created file that remains empty while Chrome is still running. Once the child exits and the file is complete, investigate buffering if the browser response alone is delayed.
Use PHP proc_open() with separate streams and a deadline
The example below is for PHP 7.4 or later on Windows, where proc_open() accepts an argument array. It avoids cmd.exe, closes Chrome’s stdin immediately, drains stdout and stderr without waiting on just one stream, and applies a PHP-side timeout. Set the Chrome path and target URL for your server. The captured DOM is saved only after the process exits successfully.
Rank #2
<?php
$chrome = 'C:\Program Files\Google\Chrome\Application\chrome.exe';
$url = 'https://example.com/';
$profile = 'C:\inetpub\temp\chrome-job-' . bin2hex(random_bytes(8));
$outputFile = 'C:\inetpub\wwwroot\page.html';
$hardTimeoutSeconds = 45;
$command = [
$chrome,
'--headless',
'--disable-gpu',
'--no-first-run',
'--no-default-browser-check',
'--user-data-dir=' . $profile,
'--timeout=30000',
'--dump-dom',
$url,
];
$descriptors = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$options = ['bypass_shell' => true];
$started = microtime(true);
$process = proc_open($command, $descriptors, $pipes, null, null, $options);
if (!is_resource($process)) {
throw new RuntimeException('Could not start Chrome');
}
fclose($pipes[0]); // Chrome does not need request input.
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$exitCode = null;
$timedOut = false;
while (true) {
$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
$status = proc_get_status($process);
if (!$status['running']) {
$exitCode = $status['exitcode'];
break;
}
if (microtime(true) - $started > $hardTimeoutSeconds) {
$timedOut = true;
proc_terminate($process);
break;
}
usleep(100000);
}
// Drain anything written just before exit or termination.
$stdout .= stream_get_contents($pipes[1]);
$stderr .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$closeCode = proc_close($process);
$elapsed = microtime(true) - $started;
// In production, send these fields to a protected application log.
error_log(json_encode([
'timed_out' => $timedOut,
'exit_code' => $exitCode,
'close_code' => $closeCode,
'elapsed_seconds' => round($elapsed, 2),
'stderr' => $stderr,
]));
if ($timedOut) {
throw new RuntimeException('Chrome exceeded the PHP deadline');
}
if ($exitCode !== 0) {
throw new RuntimeException('Chrome failed; inspect the protected log');
}
if (file_put_contents($outputFile, $stdout, LOCK_EX) === false) {
throw new RuntimeException('Could not write the DOM output file');
}
?>
Use a destination outside the publicly served directory for logs, and avoid logging the full URL if it contains private query data. The code treats Chrome’s process exit status as the success signal; validate the resulting DOM for your own application as well. PHP’s process status and proc_close() return values can differ depending on when the exit code is collected, so retain the status captured when the process first reports that it has stopped rather than assuming proc_close() alone is authoritative.
This timeout calls proc_terminate() for the process PHP started. Chrome may also create subprocesses; verify whether any remain after timeout and arrange process-tree cleanup appropriate to your Windows deployment. Do not assume terminating one process necessarily terminates every Chrome child. Keep the per-job profile unique, and ensure failed jobs cannot accumulate abandoned processes or profile directories.
If a shell is unavoidable
Prefer an argument array and bypass_shell where supported. If you must construct a shell command, escape every individual argument with PHP escapeshellarg()—including the executable path, URL, profile path, and output path. Never concatenate a URL or other request-controlled value directly into command syntax: shell metacharacters can alter the command and create command-injection risk. Shell quoting rules on Windows are easy to get wrong, so direct process invocation is the safer diagnostic and implementation path.
Distinguish a Chrome hang from an IIS timeout
Use timestamps to separate the stages: request arrival, process start, first stdout/stderr bytes, process exit, output-file write, and response completion. If Chrome remains in the process list and there is no exit event, investigate navigation, profile, permissions, or browser compatibility. If Chrome exits but PHP does not finish, inspect the process handling and pipe-draining code. If the file is complete but the browser waits, then inspect PHP/FastCGI/client buffering. If IIS ends the request at a consistent interval, compare that interval with FastCGI request and activity limits; do not confuse IIS ending the wait with Chrome completing normally.
Do not make the first fix an unlimited timeout. A longer FastCGI or PHP deadline can hide a slow failure while tying up worker capacity. Bound the browser’s navigation and the PHP request separately, log the reason for timeout, and decide whether the application should retry, return an error, or move the capture to a background job.
Choose direct Chrome or a PHP browser library deliberately
A direct CLI call gives you a clear executable, arguments, and stdout/stderr boundary, but your application must own process lifecycle, cleanup, profile isolation, and version-compatible flags. A browser library may offer a higher-level lifecycle and logging interface, yet it still depends on Chrome, Windows permissions, and correct timeout and cleanup behavior.
Rank #4
The chrome-php/chrome project documents browser startup, timeout, no-sandbox, user-data-directory, and debug-logging options. Its project notes that testing is Linux-focused even though Windows compatibility is intended. Treat it as an option to validate on the exact Windows/PHP/IIS setup, not as established proof that it fixes this incident. Compare candidates on process isolation, timeout controls, stdout/stderr visibility, profile handling, JavaScript rendering, Chrome-version compatibility, concurrent IIS requests, and cleanup guarantees.
Or skip the browser setup
If the goal is a website screenshot rather than a server-managed Chrome process, ScreenshotNeo provides a screenshot API and MCP server. A single GET request returns an image or PDF; the request below uses the supplied cURL pattern, adapted to the page you want to capture. See the ScreenshotNeo API documentation for parameters and response details.
Recommended Free Tools
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
- Cookie/consent banners, newsletter popups, and chat widgets are removed before capture; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers report the page verdict and billing status.
- An MCP server exposes
take_screenshot,get_page_info, andcapture_pdffor Claude, Cursor, and other MCP clients. - The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
Sign up for ScreenshotNeo’s free plan to try 1,000 screenshots a month without a card.
Frequently Asked Questions
Does --dump-dom return the original HTML source?
No. It serializes the DOM after Chrome has parsed the page and run scripts that may change it.
Can an IIS restart permanently fix a Chrome process that hangs?
No. It can end the current process chain, but the underlying trigger can recur on the next request.
Which details should I record before comparing two runs?
Record the Windows, IIS, PHP, and Chrome versions, application-pool identity, exact flags, target URL, profile path, and FastCGI limits.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

