CobaltPDF.WebKit 1.6.3

Prefix Reserved
dotnet add package CobaltPDF.WebKit --version 1.6.3
                    
NuGet\Install-Package CobaltPDF.WebKit -Version 1.6.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CobaltPDF.WebKit" Version="1.6.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CobaltPDF.WebKit" Version="1.6.3" />
                    
Directory.Packages.props
<PackageReference Include="CobaltPDF.WebKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CobaltPDF.WebKit --version 1.6.3
                    
#r "nuget: CobaltPDF.WebKit, 1.6.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CobaltPDF.WebKit@1.6.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CobaltPDF.WebKit&version=1.6.3
                    
Install as a Cake Addin
#tool nuget:?package=CobaltPDF.WebKit&version=1.6.3
                    
Install as a Cake Tool

CobaltPDF.WebKit

Headless WebKitGTK-based HTML → PDF renderer for .NET 8.

cobaltpdf.com · Documentation · Quick start · Pricing

A drop-in, API-compatible replacement for CobaltPDF (the Chromium-based one) designed for low-memory, high-throughput PDF rendering on Linux dynos.

dotnet add package CobaltPDF.WebKit
using CobaltPdf;                                    // same namespace as CobaltPDF
var pdf = await new CobaltEngine()
    .WithPaperFormat("A4")
    .RenderUrlAsPdfAsync("https://example.com");
await pdf.SaveAsAsync("out.pdf");

New in 1.6.0

Render output is byte-for-byte unchanged by the resilience changes; the image/viewport features are opt-in.

Pool resilience & observability (production hardening):

  • Auto-sized concurrencyPoolOptions.MaxSize now defaults to a host-aware value (the smaller of the vCPU count and how many workers fit in ~75 % of detected memory, clamped 1–8) instead of a fixed 5 — safe on memory-constrained hosts (an 8-vCPU/2 GB container caps at ~3 rather than OOMing).
  • BackpressurePoolOptions.MaxQueueDepth + the new PoolBusyException let a saturated pool fast-fail extra requests so you can return HTTP 503 + Retry-After instead of a slow timeout. Default 0 keeps the unbounded queue.
  • Host-aware memory capMaxMemoryMb is auto-clamped to ~80 % of detected host memory, so the soft cap fires with a clear error before the kernel OOM-kills the process.
  • MetricsCobaltEngine.GetPoolStatistics() returns live gauges and lifetime counters for a /health or metrics endpoint.

Rendering features (parity with the Chromium edition):

  • WithCompression(bool) — controls output compression (image re-encoding + font subsetting); on by default. (WithoutPostProcessing() is now a deprecated alias for WithCompression(false).)
  • WithEagerImages() — force lazy-loaded images (loading="lazy" and data-src/data-srcset) to load before rendering, without scrolling.
  • WithViewportSize(width[, height]) is now also settable via PdfRequest.

Benchmark vs Chromium-based CobaltPDF

10 warm renders across example.com, wikipedia.org/Portable_Document_Format, news.ycombinator.com. Pool size 1–2. Memory measured like-for-like — both engines sampled the same way, idle after warm-up.

Metric CobaltPDF (Chromium) CobaltPDF.WebKit Δ
Warm-mean render time 5,747 ms 2,456 ms 57% faster
Idle / steady memory ~240 MB ~130 MB ~45% lower

WebKit's edge is lower idle memory — it releases memory between renders. (Earlier releases cited a much larger figure that compared WebKit's worker-only RSS against Chromium's full footprint; the value above is a like-for-like measurement.) Reproduce with the WebKitBenchmark project; see bundle/README.md.

Backends — same NuGet, four modes, picked automatically

Host Backend
Linux x64/arm64 (glibc or musl) Inline — bundled WebKit, no Docker, no WSL, no setup
Windows with WSL2 (default) WSL — bundle inside your WSL distro, lightweight & fast
Windows / macOS with Docker Docker — auto-pull dev image, runs the same render code
Windows / macOS, opt-in Stub — placeholder PDF for offline dev

Built for Azure App Service Linux, Azure Container Apps, AKS, AWS ECS / Fargate, and any vanilla Ubuntu/Alpine container.

Install

dotnet add package CobaltPDF.WebKit

Quickstart

If you have Windows 10/11 with WSL2 installed (you almost certainly do — wsl --install Ubuntu if not), there's nothing else to set up:

using CobaltPdf;

var engine = new CobaltEngine();
var pdf = await engine
    .WithPaperFormat("A4")
    .RenderUrlAsPdfAsync("https://example.com");
await pdf.SaveAsAsync("out.pdf");

The library detects WSL, provisions the WebKit bundle inside your default distro on first render (~500 MB, cached forever in ~/.cache/CobaltPdfWebKit/ inside Ubuntu), then spawns the real WebKit subprocess via wsl.exe. Same engine, same code, same memory profile as Linux production.

To target a specific distro when you have multiple:

CobaltEngine.Configure(o => o.WslDistribution = "Ubuntu-22.04");

Windows / macOS development — Docker

If you'd rather use Docker (e.g. macOS has no WSL, or you prefer container isolation):

  1. Install Docker Desktop and start it.
  2. Same code as above. The library detects Docker and uses it automatically.

Or force Docker explicitly:

CobaltEngine.Configure(o => o.ForceDocker = true);

No WSL, no Docker — stub mode

For unit-testing controllers, content-types, file-download wiring without running real WebKit:

CobaltEngine.Configure(o => o.StubModeOnWindows = true);

Returns a tiny placeholder PDF that names the URL it would have rendered. Real rendering still happens in staging/production on Linux.

Linux production

dotnet add package CobaltPDF.WebKit
var engine = new CobaltEngine();
var pdf = await engine.RenderUrlAsPdfAsync("https://example.com");

The library auto-provisions a portable WebKit bundle on first render (/home/site/cobaltbundle on Azure App Service, ~/.cache/... elsewhere). No Docker, no WSL, no apt install. Subsequent renders skip provisioning.

Backend precedence

The auto-detect rules, in order:

  1. ForceWsl / ForceDocker (or COBALT_FORCE_WSL=1 / COBALT_FORCE_DOCKER=1) always wins. Throws if the forced backend is unavailable.
  2. Linux x64/arm64 → Inline bundle.
  3. StubModeOnWindows = true → Stub (placeholder PDF).
  4. Windows + PreferWsl = true (default) → WSL if available, else Docker.
  5. Otherwise → Docker if available, else WSL, else friendly error.

Pre-warming

Move the first-render cost off the user request path:

await CobaltEngine.PreWarmAsync();

What gets warmed depends on the selected backend: Linux fetches the inline bundle, WSL extracts the bundle into the distro, Docker pre-pulls the image.

With a progress hook:

CobaltEngine.Configure(opts =>
{
    opts.OnBundleDownloadProgress = p =>
        Console.WriteLine($"{p.Phase}: {p.BytesDownloaded:N0} / {p.TotalBytes:N0}");
});
await CobaltEngine.PreWarmAsync();

Supported runtimes (production)

Runtime ID Target hosts
linux-x64-glibc Azure App Service Linux, Container Apps, AKS, ECS, Ubuntu
linux-arm64-glibc App Service Linux arm64, Graviton, Ampere
linux-x64-musl Alpine-based Docker images
linux-arm64-musl Alpine arm64

The library detects the active runtime via libc/arch sniffing and fetches the matching bundle.

Air-gapped / offline deployments

There are three escalating options depending on how much you can / want to reach out at runtime. All of them refuse to talk to the public internet.

Option 1 — ship the bundle inside your publish output

Drop a pre-extracted bundle next to your built DLL at AppContext.BaseDirectory/cobalt-webkit-bundle/. The library auto-discovers it before any HTTPS attempt. No code or env-var setup. Useful for self-contained dotnet publish deployments to App Service / ECS where you control the published file tree.

Option 2 — point at a pre-extracted bundle anywhere on disk

Set COBALT_INLINE_HOME to the directory containing launch.sh / render.py / .ready. Library skips provisioning entirely and dispatches straight to that path. Useful when ops pre-stages the bundle on a shared mount.

Option 3 — self-host the tarball on a private feed

Build the tarball once (see Building the bundle below), upload it to your private Artifactory / S3 / GitHub Enterprise Releases, then:

CobaltEngine.Configure(o =>
{
    o.BundleBaseUrl = "https://artifactory.internal/cobalt-webkit/";
    o.BundleSha256Override = "8f1a92...e3b";
});

Or via env vars: COBALT_BUNDLE_BASE_URL, COBALT_BUNDLE_SHA256, COBALT_BUNDLE_URL (full URL override).

The provisioner accepts file:/// URLs too if you want to point at a local tarball on disk — useful while iterating before publishing.

Manual pre-stage for WSL (Windows dev)

When the customer's environment can't reach the bundle URL at all and you'd rather hand-stage the bundle into their WSL distro, bundle/stage-wsl.sh in this repo is the reference manual-staging script. Run it from inside their WSL distro once; it lays down launch.sh, render.py, and the .ready marker at ~/.cache/CobaltPdfWebKit/cobalt-webkit-bundle-linux-x64-glibc-v{version}/ — the same path the auto-download would have used. Subsequent renders take the fast path through WslBundleProvisioner and never touch the network.

You can also disable the download attempt entirely so the library fails fast if neither pre-stage path resolved:

CobaltEngine.Configure(o => o.AutoDownloadBundle = false);

Dependency pre-flight on WSL

On Windows + WSL, the first render runs a one-time dpkg check that the customer's distro has every apt package the bundle needs (libwebkit2gtk-4.1-0, python3-pikepdf, xvfb, fonts, etc.). If any are missing the library throws a single error listing every missing package with the exact apt-get install command — much friendlier than the cryptic launch.sh failure 30 seconds later. The check is cached per-process after the first success.

For CI / baked-image setups where the distro is guaranteed-provisioned out-of-band, skip the per-process probe:

CobaltEngine.Configure(o => o.SkipWslDependencyCheck = true);

Pool tuning & memory metrics

The pool defaults are tuned for an Azure App Service B1/B2 dyno (typical 1.75–3.5 GB cap). Override anything that does not fit your host:

CobaltEngine.Configure(o =>
{
    o.MinSize = 1;             // workers kept warm
    o.MaxSize = 5;             // concurrent renders cap
    o.MaxUsesPerBrowser = 5;   // recycle after N renders — bounds RSS
});

MaxUsesPerBrowser = 5 is the most important knob. WebKitGTK's worker process accumulates ~20–40 MB of compounding state per render (GTK image cache, Cairo glyph cache, pikepdf font tables) even with our explicit per-render WebProcess teardown. Recycling every 5 renders bounds the resident set at ~300 MB; raise it (e.g. 20) to favour throughput, lower it (e.g. 1) on tightly memory-constrained hosts at the cost of paying the ~700 ms cold-start more often.

Every render exposes both peak and steady RSS for observability:

var pdf = await engine.RenderUrlAsPdfAsync("https://example.com");
Console.WriteLine($"Render took {pdf.RenderMilliseconds} ms");
Console.WriteLine($"Peak RSS:   {pdf.PeakRssMb:F0} MB  (transient, during render)");
Console.WriteLine($"Steady RSS: {pdf.SteadyRssMb:F0} MB  (after WebKit children exited)");

Use steady RSS to size your dyno's memory limit — peak is a brief transient during page-load + PDF serialisation.

Why WebKit instead of Chromium?

Memory and speed. On the benchmark above WebKitGTK renders in ~1.9 s warm-mean vs ~5.6 s for Chromium, and uses roughly half the idle memory (~130 MB vs ~240 MB, measured like-for-like). On Azure App Service Linux B-series and Functions Consumption plans (1.5–3.5 GB caps), the headroom is the difference between "works reliably" and "OOM-killed on image-heavy pages."

Trade-offs: Chromium has broader compatibility with bleeding-edge CSS features (e.g. brand-new viewport units, container queries shipped this quarter). WebKitGTK is one release behind on average, which is fine for the static-site / receipt / report / dashboard workloads that dominate server-side PDF generation.

License

Commercial software — © 2026 Modus Squared Ltd. All rights reserved. Not open source; see LICENSE.txt for the full terms.

Free to evaluate indefinitely — no time limit, no feature restrictions. Every render path, configuration option, and cloud preset works without a license key. The only difference: unlicensed PDFs include a watermark.

When you're ready to ship to production, activate a license key to remove the watermark:

CobaltEngine.SetLicense("YOUR-LICENSE-KEY");

One CobaltPDF license key activates both the WebKit and Chromium editions. View pricing

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.6.3 44 6/21/2026

1.6.3 — Documentation & metadata only (no code change from 1.6.2): corrected the package README (license is commercial, not MIT; idle-memory figures restated like-for-like instead of the old worker-only-vs-full-footprint comparison) and added links back to cobaltpdf.com.
1.6.2 — Fixes a worker-recycle memory leak on Linux/container hosts (render output unchanged). Each render worker boots an Xvfb virtual display; on the normal recycle path (a worker reaching MaxUsesPerBrowser exits itself) its Xvfb was orphaned rather than terminated, leaking ~13 MB per recycle — unbounded over a long-running host, and worst at a low MaxUsesPerBrowser (recycling every render leaked an Xvfb every render). The pool now reaps each worker's Xvfb on dispose. Does not affect Windows/WSL dev hosts with WSLg, which start no Xvfb.
1.6.1 — Docker reliability & diagnostics (render output unchanged). In a container without the host's webkit2gtk runtime, the bundle falls back to proot, whose ptrace path-remapping can HANG under Docker's default seccomp; the engine now logs the cause and the fix up front instead of failing silently. Recommended Docker recipe: an Ubuntu 24.04 base image with `apt-get install -y libwebkit2gtk-4.1-0 xvfb` (see https://cobaltpdf.com/guides/docker). No change to managed Linux hosts (Azure App Service / Functions / Container Apps), where the self-provisioning bundle runs as before.
1.6.0 — Pool resilience & observability (render output unchanged). MaxSize now auto-sizes to the host by default — the smaller of the vCPU count and how many workers fit in ~75% of detected memory (~450 MB each), clamped 1–8 — so it stays safe on memory-constrained hosts instead of OOMing. New PoolOptions.MaxQueueDepth enables backpressure: when the pool is saturated and the queue is full, renders fast-fail with the new PoolBusyException so hosts can return HTTP 503 instead of a slow timeout. MaxMemoryMb is now auto-clamped to ~80% of detected host memory so the soft cap fires with a clean error before the kernel OOM-kills the process. New CobaltEngine.GetPoolStatistics() exposes live pool gauges and lifetime counters for health/metrics endpoints. Transient single-render retry-on-worker-crash hardened.