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
<PackageReference Include="CobaltPDF.WebKit" Version="1.6.3" />
<PackageVersion Include="CobaltPDF.WebKit" Version="1.6.3" />
<PackageReference Include="CobaltPDF.WebKit" />
paket add CobaltPDF.WebKit --version 1.6.3
#r "nuget: CobaltPDF.WebKit, 1.6.3"
#:package CobaltPDF.WebKit@1.6.3
#addin nuget:?package=CobaltPDF.WebKit&version=1.6.3
#tool nuget:?package=CobaltPDF.WebKit&version=1.6.3
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 concurrency —
PoolOptions.MaxSizenow 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). - Backpressure —
PoolOptions.MaxQueueDepth+ the newPoolBusyExceptionlet a saturated pool fast-fail extra requests so you can return HTTP 503 + Retry-After instead of a slow timeout. Default0keeps the unbounded queue. - Host-aware memory cap —
MaxMemoryMbis auto-clamped to ~80 % of detected host memory, so the soft cap fires with a clear error before the kernel OOM-kills the process. - Metrics —
CobaltEngine.GetPoolStatistics()returns live gauges and lifetime counters for a/healthor 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 forWithCompression(false).)WithEagerImages()— force lazy-loaded images (loading="lazy"anddata-src/data-srcset) to load before rendering, without scrolling.WithViewportSize(width[, height])is now also settable viaPdfRequest.
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
Windows development — WSL (recommended)
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):
- Install Docker Desktop and start it.
- 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:
ForceWsl/ForceDocker(orCOBALT_FORCE_WSL=1/COBALT_FORCE_DOCKER=1) always wins. Throws if the forced backend is unavailable.- Linux x64/arm64 → Inline bundle.
StubModeOnWindows = true→ Stub (placeholder PDF).- Windows +
PreferWsl = true(default) → WSL if available, else Docker. - 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.
Links
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 | Versions 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. |
-
net8.0
- CobaltPDF.Requests (>= 1.6.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- PDFsharp (>= 6.2.4)
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.