PDFPipe

Serverless functions / AWS Lambda

wkhtmltopdf on AWS Lambda: a binary with no libraries to link against

"error while loading shared libraries: libXrender.so.1: cannot open shared object file", or the process exits with no output and no error at all.

What is actually wrong

wkhtmltopdf is a Qt application. Even the headless build links against X11 rendering and font libraries, and the Lambda base image does not ship them, because nothing else on a serverless runtime needs an X server's client libraries. The binary is present, executable, and cannot start.

What AWS Lambda gives you to work with

a function that is handed a request, runs, and is frozen again. The execution environment is reused between invocations but you do not control when it is torn down.

  • Packaging: 250 MB unzipped for the function code and every layer attached to it, combined. That is the hard number most PDF deployments run into. A container image raises it to 10 GB but changes how the function is built and pushed.
  • Filesystem: the deployment package is read-only. Only /tmp is writable, and it defaults to 512 MB. Anything the process writes there survives for the life of the execution environment, not the invocation, which is a subtle source of both caching wins and stale-state bugs.
  • Processes: a child process can be spawned and it lives while the invocation does. When the invocation returns, the environment is frozen mid-execution rather than terminated, so a process that has not exited is simply suspended and resumed on the next call.

The fix, if you are keeping wkhtmltopdf

Build a layer on the same base image Lambda runs, copy in the binary along with every shared object it resolves, and point the dynamic linker at them. Then add fonts, because wkhtmltopdf ships none and the base image has none either.

bash
# In a container built FROM the Lambda base image, so the shared
# objects match the runtime that will load them.
ldd /usr/local/bin/wkhtmltopdf   # every line here has to come with you

# Layer layout: /opt/bin/wkhtmltopdf, /opt/lib/*.so, /opt/fonts/*
# Then, on the function:
#   LD_LIBRARY_PATH=/opt/lib:$LD_LIBRARY_PATH
#   FONTCONFIG_PATH=/opt/fonts

What still hurts after that

Without a fontconfig cache every render rebuilds the font list, which is slow on a cold start and produces empty boxes instead of text if it fails. The layer is compiled against one base image, so an AWS runtime upgrade breaks it and the breakage looks like a random rendering fault. Underneath all of it, the engine is a fork of WebKit that stopped tracking the web a long time ago, so flexbox and grid layouts do not render the way the browser you designed them in does.

What wkhtmltopdf is genuinely good at

it is a single binary with no runtime, so once it is present it does not care what language your application is written in. That is worth keeping in view: the problem on this page is where it runs, not what it produces. wkhtmltopdf is a command line tool built on an old fork of the Qt WebKit engine, invoked as a subprocess with an HTML file and an output path. It needs the binary itself, the X11 and font shared libraries it links against even in headless mode, and a font configuration, because it ships with no fonts of its own.

The other shape of the answer

Every fix above keeps the renderer inside your deployment, which means the constraint stays yours: the package size, the shared libraries, the fonts, the memory, the patching. The alternative is an HTTP call from AWS Lambda to something that already has a browser in it, which turns all of the above into a request and a response. That is what this API is, and it is the reason these pages exist. Whether that trade is right depends on whether rendering is a thing you want to operate.

Frequently asked

Can wkhtmltopdf run on AWS Lambda at all?

Build a layer on the same base image Lambda runs, copy in the binary along with every shared object it resolves, and point the dynamic linker at them. Then add fonts, because wkhtmltopdf ships none and the base image has none either. Whether that is worth doing is a separate question from whether it is possible, and the section above on what still hurts is the honest answer to it.

Why does it work locally?

Because a development machine has root, a package manager, several gigabytes of memory, a full font set and no packaging limit. Every one of those is a thing AWS Lambda constrains. A renderer that works locally and fails on deploy has not regressed: it has met the first environment that does not give it everything.

Related failures

The same platform with a different renderer, and the same renderer on other platforms.

If the render moves off this platform, none of the above is your problem to keep solving. 100 free documents a month, and a playground that needs no signup.