Set the timeout where PHP is actually waiting. A remote HTML-to-PDF service needs an HTTP-client timeout; a renderer started on the same machine needs a child-process timeout. For Symfony HttpClient, use timeout to limit idle periods and max_duration to cap the complete request. Then check PHP, your web server or proxy, queue worker, and the PDF service for separate deadlines.
First identify which operation is hanging
“HTML-to-PDF request” can describe two different execution paths:
- Remote conversion: PHP sends HTML or a URL to an HTTP API and waits for the response. Configure the HTTP client’s limits.
- Local conversion: PHP launches Chromium, wkhtmltopdf, Gotenberg, or another executable and waits for a child process. Configure the process timeout.
Changing one limit does not change the other. A remote API can also have its own rendering deadline, while PHP and a reverse proxy can terminate the request before your client timeout is reached.
Symfony HttpClient: idle timeout versus total duration
Symfony’s documentation describes timeout as an inactivity limit: “Timeouts control how long one is willing to wait while the HTTP transaction is idle.” Data can continue arriving for longer than this value if no pause exceeds the limit. If you omit the option, PHP’s default_socket_timeout applies. The current Symfony example uses 2.5 seconds as an illustration, not as a PDF-generation recommendation. See the Symfony HTTP Client documentation.
#1 Best Overall
Use max_duration when the requirement is a wall-clock ceiling for the complete request and response. A conversion that continually streams small pieces of data can exceed an idle timeout without violating it; max_duration stops that request once its total budget is exhausted.
Basic remote conversion code
<?php
use SymfonyComponentHttpClientHttpClient;
use SymfonyContractsHttpClientExceptionTransportExceptionInterface;
$client = HttpClient::create();
$pdfServiceUrl = 'https://pdf.example.test/convert';
try {
$response = $client->request('POST', $pdfServiceUrl, [
'json' => [
'html' => '<h1>Invoice 123</h1>',
],
// Maximum idle interval while the HTTP transaction is active.
'timeout' => 10.0,
// Maximum elapsed time for this complete request and response.
'max_duration' => 45.0,
]);
// Symfony responses are lazy: transport work can fail here, not only above.
$status = $response->getStatusCode();
$pdf = $response->getContent();
file_put_contents(__DIR__ . '/invoice.pdf', $pdf);
} catch (TransportExceptionInterface $e) {
error_log('PDF transport timed out or failed: ' . $e->getMessage());
http_response_code(504);
echo 'The PDF service did not respond within the configured time.';
}
The values above are application choices. Select them from observed conversion latency, HTML complexity, the service’s limits, and the caller’s deadline. Do not copy the illustrative 2.5-second value as a universal setting.
Limit connection establishment separately
DNS resolution, TCP connection, and TLS negotiation can deserve a shorter budget than rendering. Current Symfony documentation lists max_connect_duration for that purpose and marks it as introduced in Symfony 8.1. Verify your installed Symfony version before adding it:
$response = $client->request('POST', $pdfServiceUrl, [
'timeout' => 10.0,
'max_connect_duration' => 5.0, // Symfony 8.1 and later, where supported
'max_duration' => 45.0,
]);
If your version does not expose this option, use that version’s documentation rather than guessing an option name. A connection cap cannot fix a renderer that connects successfully and then stalls.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
Local renderers: set the child-process timeout
When PHP runs a binary, Symfony Process controls the wait. The Symfony Process documentation states that the default process timeout is 60 seconds. Calling setTimeout() changes it; reaching the limit throws ProcessTimedOutException. These settings are independent of an HTTP client’s timeout. Refer to the Symfony Process documentation.
<?php
use SymfonyComponentProcessProcess;
use SymfonyComponentProcessExceptionProcessTimedOutException;
$process = new Process([
'/usr/bin/chromium',
'--headless',
'--disable-gpu',
'--print-to-pdf=/tmp/invoice.pdf',
'/var/www/app/invoice.html',
]);
$process->setTimeout(120.0);
try {
$process->mustRun();
if (!is_file('/tmp/invoice.pdf')) {
throw new RuntimeException('Renderer exited without producing a PDF.');
}
} catch (ProcessTimedOutException $e) {
error_log('Renderer exceeded its 120-second process budget.');
// Remove partial output and return a controlled failure to the caller.
@unlink('/tmp/invoice.pdf');
http_response_code(504);
} catch (Throwable $e) {
error_log('Renderer failed: ' . $e->getMessage());
http_response_code(500);
}
For asynchronous execution, Symfony Process requires regular calls to checkTimeout(); otherwise your application may not notice the deadline promptly. A larger process timeout also cannot repair invalid command-line arguments, missing fonts, blocked assets, or a browser waiting forever for page activity.
Make the timeout fit the whole request budget
Choose a deadline from the outside in. The user-facing request, queue job, or webhook has a finite budget that must include conversion, retries, and any delay between attempts.
- Find the outer deadline. Check the PHP execution setting, web-server and reverse-proxy upstream timeouts, load balancer, queue-worker visibility timeout, and the remote PDF service’s documented limit. PHP’s connection behavior when its time limit is reached is described in the PHP connection-handling documentation; the exact limits vary by deployment.
- Reserve time for application work. Leave room to validate input, upload HTML, store the PDF, and send the response after conversion.
- Set the per-attempt idle limit. This catches a silent socket without cutting off a healthy stream of data.
- Set the total duration. Keep
max_durationbelow the caller’s remaining budget. - For local rendering, set the process limit below the worker or HTTP deadline so PHP can terminate cleanly and report an error.
For example, if a queue job allows 90 seconds, a 45-second conversion attempt may be reasonable only if the job also has enough time for preparation and cleanup. There is no documented universal PDF timeout; measure your own normal and worst-case documents.
Retries: per-attempt limits are not a total limit
Retries multiply elapsed time. If an attempt can run for 45 seconds and two retries are allowed, the total can approach 135 seconds before adding backoff delays. Symfony 5.x documentation describes retries for selected status codes with exponential delay, but retry behavior and supported methods depend on the Symfony version; consult the Symfony 5.x HttpClient documentation for the version you run.
- Retry transient connection failures and selected server responses, not malformed HTML or authentication errors.
- Use an overall deadline in your job or controller so retries cannot outlive the caller.
- Make the conversion operation idempotent, or attach a request identifier so a late first attempt cannot create duplicate records.
- Log attempt number, elapsed time, timeout type, HTTP status, and service request ID when available.
Browser readiness can be a separate wait
A converter may wait for fonts, images, JavaScript, or a browser network-idle event before producing the PDF. Gotenberg’s Chromium conversion documentation warns that waiting for all connections to close can be unsuitable for pages using long-polling or analytics connections. See Gotenberg’s HTML-to-PDF documentation.
Align the service-side readiness rule with the page:
- Use a less strict readiness condition for applications that keep WebSockets, long-polling, or telemetry connections open.
- Prefer a specific “page is ready” selector or application signal when the converter supports it.
- Ensure external fonts and images are reachable from the renderer’s network and that authenticated assets have valid credentials.
- Set a client timeout long enough for the chosen readiness condition, but retain a hard total-duration cap.
A client-side timeout only stops waiting; it cannot make a service-side network-idle condition succeed. If the page never reaches readiness, investigate the page and converter logs rather than continually increasing PHP’s limit.
Rank #4
Diagnose common timeout and PDF failures
| Symptom | Likely cause | Action |
|---|---|---|
Exception appears at getContent(), not request() |
Symfony response laziness deferred network activity | Keep the try/catch around request creation, status/header access, and body consumption. |
| Request fails after a quiet period | Idle timeout was reached |
Inspect service logs and asset loading; increase the idle value only if the pause is expected. |
| Request runs too long despite continuous output | No total-duration cap | Add max_duration and enforce an outer application deadline. |
| “Process timed out” after about one minute | Symfony Process’s documented 60-second default | Call setTimeout() with a budget appropriate to the document and worker. |
| PDF service reports network-idle timeout | Persistent browser connections prevent the selected readiness state | Change readiness settings or page behavior; do not rely only on a larger client timeout. |
| PHP returns 504 before the client limit | Web server, proxy, queue, or PHP runtime deadline is shorter | Trace each layer and make inner budgets finish before the outer deadline. |
| Repeated attempts create duplicate PDFs | Retry is not idempotent | Use an idempotency key or reconcile outputs by a unique job ID. |
| Renderer exits successfully but no usable PDF exists | Bad input path, missing assets, permissions, or partial output | Check exit output, verify the file, validate its size and PDF signature, and remove partial files. |
Measure before changing numbers
Record separate timings for connection setup, upload, browser readiness, conversion, download, and post-processing. Compare successful and failed jobs by document size, number of external assets, JavaScript usage, and renderer version. This shows whether you need a longer idle interval, a larger total budget, or a page-level fix.
Use a correlation ID in application logs and pass it to the remote service when supported. Capture the exact timeout option, Symfony version, retry count, HTTP status, and exception class. Never log authorization headers or document contents. For asynchronous jobs, persist the deadline and stop polling when it expires instead of allowing an orphaned renderer to consume workers.
Or skip the browser setup
If your goal is a clean capture of a web page rather than maintaining a browser and PDF renderer yourself, ScreenshotNeo provides a website screenshot API and MCP server. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the result through X-Page-Verdict and X-Billed headers. It can return PNG, JPEG, WebP, or PDF, and its MCP tools (take_screenshot, get_page_info, and capture_pdf) work with Claude, Cursor, and other MCP clients.
The API accepts full-page capture, lazy-image loading, element selectors, dark mode, device presets or custom viewports, retina scale, PDF paper and margin settings, custom CSS and JavaScript, clicks, waits, blocked requests, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching TTLs, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Existing parameter names used by other screenshot APIs also work, which can simplify migration. Details and option names are in the ScreenshotNeo documentation.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsOne-call examples
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo’s Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to try it without a card.
Frequently Asked Questions
Should I use only timeout or only max_duration in Symfony HttpClient?
Use both when you need protection from a silent connection and a hard wall-clock ceiling. They control different conditions: inactivity versus total transaction time.
Does increasing PHP’s execution limit increase a PDF service’s own limit?
No. PHP, the HTTP client, the renderer, the proxy, and the remote service can each enforce separate deadlines. Change the layer that is ending the wait.
Why can a page that works in a browser still time out during PDF conversion?
The renderer may lack authentication, network access, fonts, or a readiness signal, or the page may keep persistent connections open. Inspect converter and browser logs before increasing the timeout.
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.

