ScreenshotNeo

BlogHow-to

wkhtmltoimage on AWS Lambda: Package and Run Website Screenshot Jobs

Package wkhtmltoimage for Lambda with its native libraries and fonts, write a screenshot handler, and choose between a layer, container image, or another renderer.

By the ScreenshotNeo team4 October 202611 min read

To run wkhtmltoimage on AWS Lambda, package a Linux binary that matches the function’s operating environment and architecture, include its required system libraries and fonts, and write generated files under /tmp. You can bundle the files in the function, use a Lambda layer, or build a Lambda container image. The upstream project documents an Amazon Linux 2 Lambda archive for the first two approaches, but it does not establish compatibility with every current Lambda runtime and architecture. Verify the exact combination you deploy.

This guide shows the packaging choices, a Python Lambda handler that invokes the binary, deployment constraints, failure diagnosis, and when an older WebKit renderer is a poor fit. The code is a deployment template; it is not a claim that this particular package or sample was tested in a live Lambda deployment.

1. Choose a packaging route

Route Use it when Check before deployment
Amazon Linux 2 archive as a layer You want the upstream documented Lambda archive and a separate dependency bundle. Confirm the archive matches your function’s runtime environment and architecture. Lambda layer count, package size, and update workflow matter.
Bundle the archive with the function You want one deployment artifact and can manage the binary and dependency files alongside the handler. Keep the package within Lambda deployment limits and verify native libraries and fonts are present.
Lambda container image You need more control over system packages, fonts, or the build environment. Use a Linux image compatible with Lambda. It must support Lambda’s runtime API, and the filesystem must work read-only except for /tmp.
Use a different renderer The page depends on modern JavaScript, current browser behavior, or a more actively maintained engine. The wkhtmltopdf maintainer suggests considering Puppeteer for dynamically rendered pages. Compare deployment size, startup, security, and fidelity for your workload.

The upstream project’s downloads page documents an Amazon Linux 2 Lambda archive and shows LD_LIBRARY_PATH=/opt/lib and FONTCONFIG_PATH=/opt/fonts in its example. Those paths are specific to that archive layout; inspect the package you use and adjust paths accordingly. A layer’s files are exposed under /opt at runtime.

The project’s downloads page identifies 0.12.6 as its stable series, released June 11, 2020. Its build notes explain that Qt is statically linked in its static build, but other system packages remain dependencies. Do not assume that “static” means no shared libraries or font configuration are needed.

2. Package the binary and dependencies

Layer or bundled files

  1. Choose the upstream Lambda archive or another build intended for a compatible Linux distribution and architecture.
  2. Inspect its directory structure. Confirm the executable, shared libraries, font files, and fontconfig configuration are included.
  3. For the documented archive layout, configure LD_LIBRARY_PATH=/opt/lib and FONTCONFIG_PATH=/opt/fonts. Set these in the Lambda environment or in the handler environment.
  4. Set the executable path to the actual location in the archive, for example /opt/bin/wkhtmltoimage if that is where your archive places it.
  5. Invoke the executable with a small known page and check both the process exit status and output file before routing traffic to the function.

Do not copy an arbitrary desktop Linux or Alpine binary and expect it to run. The executable’s libc and shared-library requirements, Lambda’s operating system, and the selected CPU architecture must line up. A fontconfig executable or configuration may also expect paths that differ from the archive’s defaults.

Container image

A Lambda container image makes the operating system packages and fonts part of a versioned image. AWS base images include runtime components; an OS-only or alternative base image needs a Lambda runtime interface client. AWS requires Linux container images and says they must run on a read-only filesystem apart from writable /tmp. Upload the image to ECR in the same AWS Region as the function. The documented maximum uncompressed image size is 10 GB.

The following Dockerfile is a structural template, not a verified recipe for a particular wkhtmltoimage archive. Put a compatible extracted package in lambda-bundle/ with the paths shown, then validate it against the Lambda runtime and architecture you select.

# Dockerfile
FROM public.ecr.aws/lambda/python:3.12

# Expected bundle layout:
# lambda-bundle/bin/wkhtmltoimage
# lambda-bundle/lib/...
# lambda-bundle/fonts/...
# lambda-bundle/etc/fonts/...
COPY lambda-bundle/ /opt/
COPY app.py ${LAMBDA_TASK_ROOT}/app.py

ENV WKHTMLTOIMAGE=/opt/bin/wkhtmltoimage \
    LD_LIBRARY_PATH=/opt/lib \
    FONTCONFIG_PATH=/opt/etc/fonts

CMD ["app.handler"]

Make the executable bit available in the image, and include any additional libraries required by the binary. If the archive has different paths, update the Dockerfile and environment variables rather than copying this layout literally. Local container testing with AWS’s Lambda Runtime Interface Emulator is a documented way to exercise the runtime interface; it does not by itself prove that a particular renderer build works in production.

3. Write a Lambda handler

This Python handler accepts a URL, invokes wkhtmltoimage without a shell, saves the result under /tmp, and returns a base64-encoded PNG for a synchronous caller. Set WKHTMLTOIMAGE to the executable’s actual path. The page and network access are controlled by the renderer and the Lambda environment; validate URLs and restrict network reach independently.

# app.py
import base64
import os
import subprocess
import tempfile
from urllib.parse import urlparse

WKHTMLTOIMAGE = os.environ.get("WKHTMLTOIMAGE", "/opt/bin/wkhtmltoimage")


def validate_url(value):
    parsed = urlparse(value or "")
    if parsed.scheme not in ("http", "https") or not parsed.hostname:
        raise ValueError("url must be an absolute http or https URL")
    # Production systems should also reject private, loopback, link-local,
    # and otherwise disallowed destinations after DNS resolution.
    return value


def handler(event, context):
    url = validate_url(event.get("url"))
    os.environ.setdefault("LD_LIBRARY_PATH", "/opt/lib")
    os.environ.setdefault("FONTCONFIG_PATH", "/opt/fonts")

    with tempfile.NamedTemporaryFile(suffix=".png", dir="/tmp", delete=False) as output:
        output_path = output.name

    try:
        remaining_ms = context.get_remaining_time_in_millis()
        timeout_seconds = max(1, min(remaining_ms / 1000 - 2, 900))
        result = subprocess.run(
            [WKHTMLTOIMAGE, "--format", "png", url, output_path],
            check=False,
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            env=os.environ.copy(),
        )
        if result.returncode != 0:
            raise RuntimeError(
                "wkhtmltoimage failed: " + (result.stderr[-3000:] or "no stderr")
            )

        with open(output_path, "rb") as image_file:
            image_bytes = image_file.read()
        return {
            "statusCode": 200,
            "headers": {"Content-Type": "image/png"},
            "isBase64Encoded": True,
            "body": base64.b64encode(image_bytes).decode("ascii"),
        }
    finally:
        try:
            os.remove(output_path)
        except FileNotFoundError:
            pass

The response shape is suitable for a Lambda proxy integration configured to handle binary responses. Direct synchronous Lambda request and response payloads are each limited to 6 MB; base64 encoding adds overhead, so a screenshot can exceed the usable image size before reaching that limit. For larger outputs, write the file to object storage and return a reference instead of embedding the bytes.

Invoke and inspect the result

For a Lambda function named capture-page, a direct invocation payload can be sent with the AWS CLI:

aws lambda invoke \
  --function-name capture-page \
  --payload '{"url":"https://example.com"}' \
  --cli-binary-format raw-in-base64-out \
  response.json

For a layer-based deployment, the handler can be the same if WKHTMLTOIMAGE points to the layer’s actual executable path. Set the documented library and font paths in the function configuration, and ensure the layer’s architecture and runtime environment match the function.

4. Configure Lambda for screenshot work

Setting What to plan for
Timeout Standard invocation maximum is 900 seconds (15 minutes). Set a timeout that covers navigation, rendering, and output handling, while keeping the subprocess timeout below the remaining invocation time.
Memory Lambda allows 128 MB to 10,240 MB. Large pages, long documents, many images, and concurrent subprocesses increase memory use. Measure your own workload.
Temporary storage /tmp is writable and configurable from 512 MB to 10,240 MB. Keep outputs and temporary files there, and remove them when finished. Do not write to the container’s read-only locations.
Architecture Use a binary and dependencies built for the function architecture. The dossier does not establish a compatibility matrix for all archives, runtimes, and architectures.
Payloads Synchronous request and response payloads are each limited to 6 MB. Avoid returning large screenshots inline; store them externally and return a reference.
Concurrency Each invocation can consume CPU, memory, network, and temporary storage while rendering. Bound concurrency to protect downstream sites and control resource use.

Lambda’s current quotas page documents the limits above. They are ceilings, not recommended settings for every screenshot job.

5. Handle real-world pages and failures

Network and page behavior

  • A page can be slow because of its server, redirects, large resources, or scripts that do not settle. Set an invocation timeout appropriate to the job and make the caller’s retry behavior explicit.
  • Some pages block automated requests, require authentication, or render differently without a full browser environment. A successful process exit does not guarantee that the image contains the expected page.
  • Validate the input URL and restrict egress where possible. A user-controlled URL can otherwise direct the worker toward internal services or sensitive network destinations. Resolve and check destination addresses, account for redirects and DNS changes, and enforce network-level controls; wkhtmltoimage does not provide these safeguards for you.
  • Do not render untrusted HTML or JavaScript in a privileged environment. The project maintainer warns that malicious input can lead to server takeover and recommends sanitization and mandatory access controls such as AppArmor or SELinux.
  • Set a stable viewport and relevant renderer options when repeatability matters. Remote pages can still change over time, and fonts or external resources can affect output.

Common errors

Symptom Likely cause Fix
No such file or directory when the executable exists A required dynamic loader or shared library is missing, or the binary targets another Linux environment. Inspect the binary’s dependencies in a compatible build environment. Use a distribution-compatible package and include required libraries; verify the actual executable path.
error while loading shared libraries The runtime cannot find a required library. Include the library and set LD_LIBRARY_PATH to the directory containing it. Confirm the archive’s directory layout.
Text is blank, substituted, or has incorrect glyphs No suitable fonts are installed, fontconfig cannot find its configuration, or font caches/configuration are missing. Include fonts and fontconfig configuration, set FONTCONFIG_PATH to the right directory, and verify the requested font is available.
Permission denied The executable bit is missing or the file is on a non-executable mount. Preserve executable permissions in the layer or image and run it from the packaged path.
Works locally but not in Lambda Different architecture, libc or system libraries, environment variables, read-only paths, or fonts. Build and inspect against a Lambda-compatible Linux environment. Reproduce the runtime interface locally and write only to /tmp.
Timeout or incomplete page Slow navigation, blocked resources, lengthy scripts, or an unsuitable timeout. Inspect stderr and the rendered output, tune the invocation timeout and workload, and decide whether the page is suitable for this renderer.
Image is too large to return The encoded response exceeds the synchronous payload limit. Store the image in object storage and return a location or identifier.
Modern page is missing content or styled incorrectly The page relies on JavaScript or browser features that this older Qt WebKit engine does not support. Use a renderer with a more suitable browser engine. The project maintainer suggests Puppeteer for dynamic JavaScript pages.

6. Performance, reliability, and cost

There is no reliable universal render-time estimate in the cited sources. Page complexity, remote resource latency, image size, available memory, and initialization work all affect a job. Measure representative pages in your own Lambda configuration, including cold starts and failure cases. Avoid claiming a speed or cost advantage without measurements for your workload.

Reuse the packaged binary and configuration rather than downloading them during every invocation. Use bounded retries for transient network failures, and make jobs idempotent if callers can retry after timeouts. Capture exit status and a limited stderr excerpt in logs; avoid logging credentials, cookies, or sensitive page content. A renderer timeout and a Lambda timeout should be coordinated so the handler has time to report or clean up.

Lambda charges depend on the configured resources and invocation usage under your AWS account’s pricing. Larger memory settings may change execution characteristics and cost; evaluate with measured duration and your applicable regional pricing. Keep temporary storage sized for concurrent outputs and page assets. For large batches, queue work and limit worker concurrency rather than creating unbounded simultaneous browser processes.

7. When wkhtmltoimage is the wrong renderer

wkhtmltoimage is based on Qt WebKit and does not require a display server. Its stable series is old: the project lists 0.12.6, released in 2020. The maintainer’s status page says the Qt 4 series has been unsupported since 2015 and that its WebKit had not been updated since 2012. These are the project’s own published status statements, not a new independent security audit.

That age matters when a screenshot must match a modern browser, when a site depends on dynamic JavaScript, or when processing untrusted pages. Review the security implications and isolate the renderer. If current browser behavior is essential, compare a maintained browser automation approach such as Puppeteer, as the maintainer recommends for dynamic pages. The best choice depends on your fidelity, deployment size, startup, and security requirements.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Instead of packaging a renderer and its native dependencies, make one GET request. See the API documentation for configuration details.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie banners are accepted like a visitor and removed along with 60+ known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An 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 screenshots. Every feature is on every plan.

Sign up free for 1,000 screenshots a month, no card required.

FAQ

Does wkhtmltoimage need X11 or a display server in Lambda?

No. The project describes it as a headless HTML-to-image tool based on Qt WebKit.

Can I use an Amazon Linux 2 layer with any Lambda runtime?

The upstream project documents an Amazon Linux 2 Lambda archive, but the available sources do not provide a current compatibility matrix for every runtime and architecture. Check the package against your exact function configuration.

Can wkhtmltoimage capture pages that require JavaScript?

It can execute some page scripts, but its old WebKit engine may not match modern browser behavior. For pages whose content depends on dynamic JavaScript, the maintainer suggests considering Puppeteer.

Should the screenshot be returned directly from Lambda?

Only if the encoded response fits the synchronous payload limit. For larger results, write the image to object storage and return a reference.

Primary sources