Both engines · validated on real Azure infrastructure
Engine:
No container needed. CobaltPDF.WebKit deploys to a stock Linux
Functions plan as a plain ~3 MB zip — its self-contained render bundle downloads on first
start. Validated end-to-end on stock Linux plans; B2 (3.5 GB) is the recommended
minimum for real-world pages.
Linux plans require a custom container. Azure's stock Linux Functions image
does not include Chromium's system libraries and they cannot be installed on a code-only plan.
Windows plans work with a normal code deploy. Prefer zero-container Linux?
1 Choose a plan
Consumption Plan is not supported by either engine.
It aggressively recycles instances and has limited memory — Chromium cannot start reliably.Its writable temp disk is smaller than the extracted render bundle (~611 MB).
Windows plan — any Dedicated (B1+) or Premium plan; Chromium's bundled
Windows binary runs natively with a normal code deploy.
Linux plan — use Premium EP1 or higher, or a Dedicated plan
with at least 3.5 GB memory, and deploy as a custom
container (step 5). 1.75 GB B1 instances are below Chromium's floor — renders
OOM and fail intermittently under load.
Any stock Linux plan with Always On. We recommend
B2 (3.5 GB) as the minimum for production:
B2 (3.5 GB, ~£20/mo) — recommended minimum. Gives one warm worker real headroom above the ~300–400 MB a typical render peaks at, so image-rich content pages and dashboards render without memory pressure.
B3 / EP1+ / Dedicated ≥ 7 GB — for concurrency, very heavy pages, and lower latency under load. For faster per-render latency specifically, choose Premium v3 (Pv3): its dedicated cores are far quicker than the burstable Basic core.
B1 (1.75 GB) — only for light, self-contained HTML you control (invoices, receipts, statements). Heavy or image-rich third-party pages can exceed its memory and get OOM-killed mid-render, and its single burstable core makes renders slow. Not recommended for rendering arbitrary URLs.
On Linux, apply the Azure preset. On Windows, no preset is needed.
Linux plan
Program.cs
using CobaltPdf;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
var host = newHostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddCobaltPdf(o =>
{
CloudEnvironment.ConfigureForAzure(o);
o.MaxSize = 1; // 1 on EP1 (3.5 GB); 2 on EP2+ (7 GB)
});
})
.Build();
CobaltEngine.SetLicense(
Environment.GetEnvironmentVariable("COBALTPDF_LICENSE")!);
awaitCobaltEngine.PreWarmAsync();
host.Run();
Windows plan
Program.cs
using CobaltPdf;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
var host = newHostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// No cloud preset needed on Windows
services.AddCobaltPdf(o =>
{
o.MaxSize = 2;
});
})
.Build();
CobaltEngine.SetLicense(
Environment.GetEnvironmentVariable("COBALTPDF_LICENSE")!);
host.Run();
Size the pool to the plan, soften the failure modes, and pre-warm at startup so the one-time
bundle download (~262 MB) happens off the request path.
Program.cs
using CobaltPdf.WebKit;
using Microsoft.Extensions.Hosting;
var host = newHostBuilder()
.ConfigureFunctionsWorkerDefaults()
.Build();
CobaltEngine.SetLicense(
Environment.GetEnvironmentVariable("COBALTPDF_LICENSE")!);
CobaltEngine.Configure(o =>
{
o.MinSize = 1;
o.MaxSize = 1; // 1 on B2 (3.5 GB); 2 on B3/EP2 (7 GB)
o.MaxMemoryMb = 2048; // clean error instead of OOM-kill
o.BrowserLeaseTimeout = TimeSpan.FromSeconds(180); // queued requests outlive a slow render
});
// Bundle download + extract + worker warm-up, off the request path.
_ = Task.Run(() => CobaltEngine.PreWarmAsync());
host.Run();
4 App settings
One setting is critical on App Service infrastructure:
Azure CLI
az functionapp config appsettings set -n <APP> -g <RG> --settings \
COBALT_BUNDLE_CACHE_DIR=/tmp/cobaltbundle \
COBALTPDF_LICENSE="YOUR-LICENSE-KEY" \
SCM_DO_BUILD_DURING_DEPLOYMENT=false ENABLE_ORYX_BUILD=false
COBALT_BUNDLE_CACHE_DIR=/tmp/cobaltbundle matters. Without it, the
bundle cache lands on /home — an Azure Files network share where extracting
thousands of files is 10–100× slower than local disk, and the first render can time
out. /tmp is instance-local: each new instance downloads once (~40 s), then
caches for its lifetime.
Outbound HTTPS to github.com and release-assets.githubusercontent.com
must be allowed (only relevant in egress-locked VNets — or self-host the bundle via
COBALT_BUNDLE_BASE_URL).
54 Create the HTTP function
Accepts HTML in the POST body and returns a PDF. The only line that differs between engines is
the using.
RenderPdf.cs
using System.Net;
using CobaltPdf;using CobaltPdf.WebKit;using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
public classRenderPdf
{
[Function("RenderPdf")]
public asyncTask<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var html = await newStreamReader(req.Body).ReadToEndAsync();
var pdf = await newCobaltEngine()
.WithPaperFormat("A4")
.RenderHtmlAsPdfAsync(html);
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "application/pdf");
await response.Body.WriteAsync(pdf.BinaryData);
return response;
}
}
Building a PDF microservice? Use the shared
CobaltPdf.Requests
wire model — clients POST a PdfRequest as JSON and never need to know which
engine the service runs.
65 Deploy
A plain code deploy to the stock plan — the artifact is ~3 MB:
First start per instance: ~40 s bundle download + ~90 s extract,
handled by PreWarmAsync off the request path. After that, renders never touch
the network. Restarts on the same instance reuse the cache.
Windows plan — code deploy
Do not use -r or --self-contained — both
flatten the runtimes/ folder and break Chromium path resolution.
curl -X POST \
"https://<APP>.azurewebsites.net/api/RenderPdf?code=<KEY>" \
-d "<h1>Hello from Azure!</h1>" \
-o output.pdf
Troubleshooting
"error while loading shared libraries: libglib-2.0.so.0"
You deployed code-only to a stock Linux plan. Chromium's system libraries are missing and
cannot be installed there — deploy as a custom container (step 5), or switch to
,
which needs no container.
Chromium path not found
Your publish or CI/CD pipeline used -r linux-x64 or --self-contained.
Remove them: dotnet publish -c Release -o ./publish.
Out of memory / intermittent render failures
The plan is too small. Chromium needs ≥ 3.5 GB per instance: MaxSize = 1
on EP1, MaxSize = 2 on EP2+. Monitor Metrics > Memory working set.
Crashes on Consumption
Consumption is not supported. Scale up to Premium (EP1+) or a Dedicated plan ≥ 3.5 GB.
Empty HTTP 500 / worker restarts on heavy pages
The render exceeded the instance's memory and Linux OOM-killed the worker process — because the
process dies, the response has no error body, just a bare 500 after a long pause. Heavy,
image-rich pages need more than B1's 1.75 GB: move to B2 (3.5 GB), or
B3 (7 GB) for very large pages. Watch Metrics > Memory working
set to confirm.
Images missing from the rendered PDF
Many sites lazy-load images as you scroll, so a single-viewport capture never fetches the ones
below the fold. Set LazyLoadPages on the request (it scrolls the page to trigger
them) and keep WaitStrategy = "networkIdle" so the image requests finish before
capture. Not Azure-specific — it applies to any host.
First render extremely slow or times out
The bundle cache is on /home (a network share). Set
COBALT_BUNDLE_CACHE_DIR=/tmp/cobaltbundle (step 4) and restart.
"render pool saturated" under concurrency
A queued request waited longer than BrowserLeaseTimeout while a slow render held
the worker. Raise it to ≥ 2× your worst-case render time (step 3), raise
MaxSize on bigger plans, or return HTTP 503 to let clients retry.
Bundle download fails
Egress to github.com / release-assets.githubusercontent.com is
blocked. Allow it, or self-host the bundle and set COBALT_BUNDLE_BASE_URL.
Fails on Consumption plan
Expected — the extracted bundle (~611 MB) exceeds the Consumption plan's writable
temp disk, and Consumption can't keep the pool warm. Use a stock Linux plan with Always On —
B2 (3.5 GB) minimum, EP1+ for interactive latency.