Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 wkhtmltopdf appears to run forever from Java, first assume a subprocess I/O deadlock: the child writes to stdout or stderr, Java does not read quickly enough, the operating-system pipe fills, and the child blocks before Java’s waitFor() can return. The reliable fix is to drain output while the process runs (usually with concurrent readers), merge or redirect the streams when appropriate, close unused stdin, and enforce a timeout.

This explains the mechanism, shows a safer ProcessBuilder implementation, and covers the less common causes—input handling, conversion failures, permissions, and executable/version problems.

Why Runtime.exec() can appear to hang

Java connects a launched process to three streams by default:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • The Java process output stream is the child’s standard input.
  • The Java process input stream carries the child’s standard output.
  • The Java process error stream carries the child’s standard error.

Those connections are normally pipes with limited buffers. If wkhtmltopdf writes enough diagnostic or progress text and Java leaves one stream unread, the buffer can become full. wkhtmltopdf then blocks on its next write. Java, waiting in waitFor(), sees a child that cannot finish. Oracle’s Java Process API warns that failing to promptly write or read process streams can block or deadlock a subprocess.

waitFor() does not drain either stream for you. Reading stdout after waitFor() is therefore unsafe when the child can produce substantial output. A matching historical report observed wkhtmltopdf output on stderr; that is useful as a clue, not a guarantee for every build or operating system.

Use ProcessBuilder instead of a new Runtime.exec() call

ProcessBuilder is the preferred API for new code. Pass each argument as a separate list item rather than constructing a shell command string. This avoids quoting errors when a URL, input path, or output path contains spaces or shell metacharacters.

import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public final class Wkhtmltopdf {
    public static int convert() throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
            "wkhtmltopdf",
            "https://example.com",
            "/tmp/output.pdf"
        );
        pb.redirectErrorStream(true); // stdout and stderr become one stream

        Process process = pb.start();
        process.getOutputStream().close(); // no HTML or arguments are sent on stdin

        Thread logReader = new Thread(() -> {
            try (var reader = process.inputReader()) {
                reader.lines().forEach(line -> System.err.println("wkhtmltopdf: " + line));
            } catch (IOException e) {
                // Record the reader failure in your application logger.
            }
        }, "wkhtmltopdf-output");
        logReader.start();

        boolean finished = process.waitFor(90, TimeUnit.SECONDS);
        if (!finished) {
            process.destroy();
            if (process.isAlive()) {
                process.destroyForcibly();
            }
            logReader.join(Duration.ofSeconds(5).toMillis());
            throw new IOException("wkhtmltopdf timed out");
        }

        logReader.join(Duration.ofSeconds(5).toMillis());
        int exitCode = process.exitValue();
        if (exitCode != 0) {
            throw new IOException("wkhtmltopdf failed with exit code " + exitCode);
        }
        return exitCode;
    }
}

The example uses one merged stream, so one reader is sufficient. Adapt the character encoding, timeout, logging destination, and termination policy to your Java version and deployment. The snippet is a pattern, not a claim that every operating system or wkhtmltopdf build behaves identically.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Drain separate stdout and stderr concurrently

If you need to preserve stdout and stderr separately, do not read one completely before starting the other. Either stream each concurrently or use an equivalent executor-based pair of tasks. Otherwise the stream you are not servicing can fill and block wkhtmltopdf.

ProcessBuilder pb = new ProcessBuilder(
    "wkhtmltopdf", "input.html", "output.pdf");
Process p = pb.start();
p.getOutputStream().close();

var stdout = java.util.concurrent.CompletableFuture.runAsync(() -> {
    try (var in = p.getInputStream()) {
        in.transferTo(System.out);
    } catch (java.io.IOException e) {
        // log reader failure
    }
});
var stderr = java.util.concurrent.CompletableFuture.runAsync(() -> {
    try (var in = p.getErrorStream()) {
        in.transferTo(System.err);
    } catch (java.io.IOException e) {
        // log reader failure
    }
});

if (!p.waitFor(90, java.util.concurrent.TimeUnit.SECONDS)) {
    p.destroyForcibly();
    throw new java.io.IOException("conversion timed out");
}
stdout.join();
stderr.join();
if (p.exitValue() != 0) {
    throw new java.io.IOException("conversion failed: " + p.exitValue());
}

For very high-volume logs, replace unbounded console output with bounded, rotating application logs. The important property is that both pipes are serviced while the child is alive.

Choose a stream-handling strategy

Requirement Approach Trade-off
One combined diagnostic log redirectErrorStream(true) and drain one stream Simple, but stdout and stderr are no longer distinguishable.
Separate diagnostics Concurrently read getInputStream() and getErrorStream() Preserves origin; requires two readers and coordinated cleanup.
Completion status only redirectOutput(file) and redirectError(file) Prevents pipe saturation while retaining logs on disk.
No logs needed Redirect both streams to an appropriate discard destination Least diagnostic information when conversion fails.

There is no established performance comparison among these choices here. Select based on whether you need stderr, how much output you expect, and your operational log policy.

Close stdin unless you intentionally send data

Java’s process.getOutputStream() is wkhtmltopdf’s stdin. If your command supplies an input URL or file as an argument and sends no data, close that stream immediately. Otherwise a child mode that reads stdin can wait for an end-of-file that never arrives.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

wkhtmltopdf documents --read-args-from-stdin as a special batch protocol: each line received on stdin is treated as a separate invocation. Do not enable it accidentally. If you do use it, write complete argument lines, flush them, and close stdin when the batch is complete.

Bound every wait and clean up on timeout

Use timed waitFor rather than an unbounded wait. A timeout is a failure state, not a successful conversion. Before terminating, preserve whatever logs and status information your application needs. Then call destroy(); if the process remains alive after a short grace period, use destroyForcibly(). Always close readers and avoid leaving orphaned child processes.

Choose the timeout from the page complexity, network conditions, and server policy. A fixed 90-second example is only a starting point; production code should make it configurable and should verify that the expected output file exists and is complete.

A diagnostic sequence for a real hang

  1. Record the launch context. Capture the exact argument list, Java version, operating system, wkhtmltopdf version, input URL or file, output path, working directory, and whether stdin is intentional.
  2. Identify the blocked operation. A thread dump can show whether Java is blocked in waitFor, reading a stream, or writing stdin. Check whether the child is still alive.
  3. Redirect output temporarily. Send stdout and stderr to files and inspect stderr first. This both prevents unread-pipe deadlock and exposes executable, network, rendering, and permission messages.
  4. Check stdin modes. Look for accidental --read-args-from-stdin and for code that keeps stdin open without sending data.
  5. Separate conversion from launching. Run the identical argument vector as the service account outside Java. Differences in PATH, HOME, fonts, certificates, sandboxing, and permissions often explain failures.
  6. Add bounded cleanup. On timeout, preserve logs, terminate the process, remove partial output according to policy, and report a distinct timeout error.

Common symptoms and fixes

Java waits forever and stderr grows

Likely cause: stderr’s pipe is full. Drain stderr concurrently, merge it with stdout, or redirect it to a file.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The child is alive but produces no output

It may be waiting for stdin, loading a slow page, or stalled in rendering. Close unused stdin, verify the URL is reachable from the same host, and enforce a timeout.

The command works in a terminal but not in Java

Use an absolute executable path or configure ProcessBuilder‘s environment and working directory. Pass arguments individually; do not rely on shell expansion or an interactive user’s PATH.

Exit code is nonzero after the deadlock is fixed

Read and retain stderr. Investigate the actual wkhtmltopdf message, input validity, output-directory permissions, missing dependencies, certificates, and version-specific rendering behavior.

Output PDF is missing or partial

Wait for a zero exit code, then verify the file exists and has an appropriate size before publishing it. A timeout or forced termination can leave a partial file.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your real requirement is a clean website image or PDF rather than wkhtmltopdf itself, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for options such as full-page capture, CSS selectors, device presets, JavaScript, custom headers, cookies, PDF settings, caching, async webhooks, and bulk capture.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Is Runtime.exec() itself defective?

No. The usual problem is unmanaged child streams and unbounded waiting. Existing code can be corrected, but ProcessBuilder makes stream and redirection choices clearer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I always merge stderr into stdout?

No. Merge when one combined log is sufficient; read both concurrently when stderr must remain distinguishable.

Can a timeout prove wkhtmltopdf is hung?

No. It establishes that the operation exceeded your deadline. Inspect logs and process state before deciding whether rendering, networking, input, or environment caused the delay.

Frequently Asked Questions

Is Runtime.exec() itself defective?

No. The usual problem is unmanaged child streams and unbounded waiting. Existing code can be corrected, but ProcessBuilder makes stream and redirection choices clearer.

Should I always merge stderr into stdout?

No. Merge when one combined log is sufficient; read both concurrently when stderr must remain distinguishable.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can a timeout prove wkhtmltopdf is hung?

No. It establishes that the operation exceeded your deadline. Inspect logs and process state before deciding whether rendering, networking, input, or environment caused the delay.

The Bottom Line

Drain or redirect both child output streams while wkhtmltopdf runs, close unused stdin, and replace unbounded waitFor() calls with timed waits and explicit cleanup. Then investigate the command’s input and execution environment if the bounded process still fails.

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.