How to Call wkhtmltopdf from Node.js
Call wkhtmltopdf from Node.js with child_process or its npm wrapper. Install the separate binary, stream PDFs safely, and troubleshoot deployment and rendering issues.
wkhtmltopdf is a separate command-line program that converts a URL or HTML document into a PDF. From Node.js, install that executable in the target environment, then invoke it asynchronously with node:child_process or use the npm wkhtmltopdf wrapper. Installing the npm package alone does not install the renderer.
This guide shows how to call wkhtmltopdf from Node.js, write a PDF to disk, pipe output, configure rendering, and diagnose common failures. The code is illustrative; verify the exact binary and runtime you deploy.
1. Install and verify the executable
- Install wkhtmltopdf for the operating system and architecture where your Node process will run.
- Check that the executable is available to the process as
wkhtmltopdf, or record its absolute path. - Run
wkhtmltopdf --versionin the same container, service account, or deployment environment used by the application.
The project downloads page identifies version 0.12.6 as the stable series and gives its release date as June 11, 2020. Its download matrix is specific to that release; it is not a guarantee of support for every current operating system or runtime. Check the chosen package and verify rendering in the actual deployment environment. wkhtmltopdf downloads
If you prefer the npm wrapper, install it separately with npm install wkhtmltopdf. The package wraps the external executable; it does not bundle it. Its README documents a command property for specifying the executable path if PATH lookup is unsuitable. npm package README
2. Call it with Node.js child_process
For a URL-to-file conversion, execFile is a direct option. It takes an executable and an argument array and does not start a shell by default. Keeping arguments separate avoids shell parsing. The asynchronous API avoids blocking the Node event loop while the renderer runs.
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
async function createPdf() {
const executable = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
const outputPath = '/tmp/report.pdf';
const args = [
'--quiet',
'--page-size', 'A4',
'--encoding', 'utf-8',
'https://example.com/report',
outputPath
];
try {
const { stderr } = await execFileAsync(executable, args, {
timeout: 30_000,
maxBuffer: 1024 * 1024
});
if (stderr) console.error('wkhtmltopdf diagnostics:', stderr);
return outputPath;
} catch (error) {
console.error('PDF conversion failed', {
code: error.code,
signal: error.signal,
exitCode: error.code,
stderr: error.stderr
});
throw error;
}
}
createPdf().then(path => console.log('PDF written:', path));
Use an absolute output path that the service account can write to. A timeout is an application policy, not a universal correct value; set it to fit the document and workload. Check that the output exists and is complete before serving it. On failure, log a bounded diagnostic and avoid exposing internal paths or stderr to an untrusted client.
In current Node.js, the rejection error carries process failure details; inspect the runtime’s child-process documentation when adapting error handling. Avoid setting shell: true, especially if any part of a command could come from a request.
3. Return the PDF from an HTTP endpoint
A reliable endpoint should finish conversion before sending success headers. If the command fails after a response has started, the server cannot replace the partial response with a clean error. This example creates a temporary file, then streams it to the client and removes it when streaming ends.
import express from 'express';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { createReadStream } from 'node:fs';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const app = express();
const execFileAsync = promisify(execFile);
app.get('/report.pdf', async (req, res, next) => {
let dir;
try {
dir = await mkdtemp(join(tmpdir(), 'pdf-'));
const pdfPath = join(dir, 'report.pdf');
const executable = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
await execFileAsync(executable, [
'--quiet', '--page-size', 'A4',
'https://example.com/report', pdfPath
], { timeout: 30_000, maxBuffer: 1024 * 1024 });
res.type('application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="report.pdf"');
const stream = createReadStream(pdfPath);
stream.on('error', next);
stream.on('close', () => {
rm(dir, { recursive: true, force: true }).catch(() => {});
});
stream.pipe(res);
} catch (error) {
if (dir) await rm(dir, { recursive: true, force: true }).catch(() => {});
next(error);
}
});
app.listen(3000);
For production, also define request limits, concurrency limits, client-disconnect behavior, and a cleanup policy for files left behind by abrupt process termination. Do not let request parameters select arbitrary URLs, local paths, or wkhtmltopdf flags without validation.
4. Stream process output when appropriate
spawn is useful when you need process streams or want to handle a large output incrementally. wkhtmltopdf accepts an output destination of - for standard output in its command-line interface. Confirm this behavior with the deployed build. The following writes stdout to a file while collecting stderr and checking the exit status.
import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';
function renderToFile(url, outputPath) {
return new Promise((resolve, reject) => {
const child = spawn('wkhtmltopdf', ['--quiet', url, '-'], {
stdio: ['ignore', 'pipe', 'pipe']
});
const output = createWriteStream(outputPath);
let stderr = '';
let settled = false;
const fail = error => {
if (settled) return;
settled = true;
output.destroy();
child.kill();
reject(error);
};
child.on('error', fail);
child.stderr.setEncoding('utf8');
child.stderr.on('data', chunk => { stderr += chunk; });
child.stdout.pipe(output);
output.on('error', fail);
output.on('finish', () => {
if (child.exitCode === 0) {
if (!settled) { settled = true; resolve({ outputPath, stderr }); }
}
});
child.on('close', (code, signal) => {
if (code !== 0) fail(new Error(`wkhtmltopdf exited ${code ?? signal}: ${stderr}`));
else if (output.writableFinished && !settled) {
settled = true;
resolve({ outputPath, stderr });
}
});
});
}
await renderToFile('https://example.com/report', '/tmp/report.pdf');
For a robust streaming implementation, add a timer that kills the child on expiry, cap retained stderr, and handle destination backpressure and client disconnects. A child-process error event indicates launch or process-management failure; a nonzero close code indicates the converter ran but failed. Never treat partial stdout as a valid PDF when the process exits unsuccessfully.
5. Use the npm wrapper
The wrapper offers a stream-oriented interface and can accept a URL or HTML input. Its API and package compatibility should be checked against the installed package version. This example pipes a URL conversion to a file:
import wkhtmltopdf from 'wkhtmltopdf';
import { createWriteStream } from 'node:fs';
wkhtmltopdf('https://example.com/report', {
pageSize: 'A4',
encoding: 'UTF-8',
command: process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf'
}).pipe(createWriteStream('/tmp/report.pdf'));
Observe the output stream’s error and finish events in application code, and consult the package README for the installed version’s callback, input stream, HTML string, and output-file forms. A pipe completing should not be your only signal if your code also needs to report child exit failures.
6. Configure pages, JavaScript, and resources
wkhtmltopdf options are command-line flags. Put each flag and its value in a separate argument entry when using execFile or spawn. The complete option set is documented in the usage manual; common areas to configure include:
| Need | Options or behavior to review |
|---|---|
| Page geometry | --page-size, --orientation, margins, header and footer settings |
| Character rendering | --encoding, installed fonts, and whether the output environment includes required font files |
| JavaScript pages | JavaScript is enabled by default; the documented default delay is 200 ms. Review the JavaScript delay setting or disable JavaScript when it is not needed. |
| Styles | Print versus screen media behavior; verify which stylesheet rules your page expects. |
| Missing resources | Load-error policy can abort, ignore, or skip depending on the selected setting; media-load error handling is configurable too. |
| Images | Image loading can be disabled; ensure it is enabled when the document needs images. |
| Local assets | Local-file access for a local input page reading other local files is disabled by default in the documented behavior. --allow grants access to specified paths. |
A fixed JavaScript delay does not prove that a single-page application has finished rendering. If the page creates content asynchronously, use a reliable application-side ready signal where possible, or render controlled HTML after data has been loaded. The wkhtmltopdf project suggests considering Puppeteer for sites that rely on dynamic JavaScript. Project status and recommendations
7. Input choices and safe HTML conversion
You can pass a remote URL, a local HTML file, or HTML through the wrapper’s input interfaces. For the CLI, use documented input syntax and an explicit output target. When rendering a local file that references stylesheets, images, or fonts on disk, narrowly allow only the directories needed. Do not grant broad filesystem access simply to make a template render.
The wkhtmltopdf project explicitly warns against processing untrusted HTML or JavaScript because it can compromise the server running the renderer. Use controlled templates and data, escape values for their output context, run the process with minimal privileges, and restrict its filesystem and network access using deployment controls. HTML escaping alone is not a sandbox for a complex renderer. The project status page also recommends considering confinement tools such as AppArmor or SELinux. Project security warning
Do not interpolate user input into a shell command. Node’s child-process documentation warns that shell-enabled execution with unsanitized input can enable arbitrary command execution. Use argument arrays and validate allowed options, URLs, and paths. Node.js child_process documentation
8. Troubleshooting
| Symptom | Likely cause | What to check or change |
|---|---|---|
ENOENT or executable not found |
The binary is not installed, or the service PATH differs from your shell. | Install the binary in the runtime image; set an absolute WKHTMLTOPDF_PATH; preserve PATH if you provide a custom environment object. |
| Permission denied | The executable or output directory is not executable/writable for the service account. | Check file permissions, directory permissions, mount options, and the identity running Node. |
| Nonzero exit code | The converter rejected an option, could not load input/resources, or hit a rendering error. | Log exit code and bounded stderr on the server; reproduce with the same binary and flags; inspect load-error policy and resource reachability. |
| Blank or incomplete output | The page may render dynamically after the configured delay, resources may fail, or the process may have been terminated. | Verify JavaScript completion and resource access; increase an appropriate delay only as a diagnostic; check child exit and confirm output completion before serving. |
| Images, CSS, or fonts missing | Remote assets are unreachable, local file access is restricted, or required fonts are absent. | Check URLs from the renderer environment, explicitly allow only required local directories, install fonts, and inspect print/screen styles. |
| Works locally but fails in deployment | Different binary build, architecture, PATH, fonts, libraries, permissions, or network rules. | Compare version and build in the actual container; test the production image and service identity rather than relying on a developer machine. |
| Hung requests or event-loop delay | No timeout/concurrency policy, synchronous child-process use, or too many simultaneous conversions. | Use asynchronous APIs, set a timeout and cancellation policy, cap concurrent conversions, and monitor process cleanup. |
| Local HTML cannot read a file | Local-file access is restricted by default in the documented case. | Use --allow /specific/path for only the needed asset directory and verify the deployed binary’s behavior. |
9. Performance, reliability, and cost
Each conversion launches an external process, so account for process startup, page loading, JavaScript execution, assets, and PDF generation in latency and capacity planning. No benchmark is established by the reviewed sources; measure with your own templates and deployment image. Avoid synchronous child-process methods in a server because they block the event loop. Reuse or cache finished PDFs only when the content and access policy make that safe.
Bound concurrency so a burst of requests cannot launch an unbounded number of renderers. Set per-job timeouts, propagate cancellation when clients disconnect if appropriate, clean temporary files, and distinguish launch errors from renderer failures. For reliability, validate a generated PDF before delivery and retain enough server-side diagnostics to reproduce failures without returning sensitive details to clients.
There is no usage price for the wkhtmltopdf executable established in the sources here. Operational costs are those of the machine, storage, maintenance, and engineering needed to install and isolate the renderer. Its stable release information is dated 2020, and the project status page describes an old Qt/WebKit base; verify whether that maintenance and compatibility profile fits your security and support requirements.
10. When to consider another renderer
The wkhtmltopdf project suggests WeasyPrint or commercial Prince for generating reports from HTML under your control, and Puppeteer for websites that depend on dynamic JavaScript. These are the project’s recommendations, not benchmark rankings. Compare output fidelity on your own templates, JavaScript completion behavior, security maintenance, platform support, deployment dependencies, streaming needs, and licensing or commercial terms. wkhtmltopdf status page
Or skip the browser setup
If your goal is a screenshot of a webpage rather than a PDF rendered by wkhtmltopdf, ScreenshotNeo provides a website screenshot API and MCP server. Make one GET request with the target URL:
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 removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed; the response includes page-verdict and billing headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the API documentation and sign up for 1,000 free screenshots a month, with no card.
FAQ
Does the npm package include wkhtmltopdf?
No. It is a Node wrapper around a separately installed executable.
Can I run wkhtmltopdf synchronously?
Node provides synchronous child-process methods, but they block the event loop. Prefer asynchronous methods in servers.
Why does a page look different in the PDF?
Check fonts, print styles, page geometry, resource access, JavaScript timing, and the exact deployed build.
Is wkhtmltopdf a modern browser engine?
The project status page describes its Qt/WebKit base as old. Assess current compatibility and security requirements against your use case before adopting it.
What if my page depends on a client-side app?
Determine whether its content is ready within the renderer’s behavior; the project recommends considering Puppeteer for dynamic JavaScript sites.


