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.

2 Create the project

Terminal
func init CobaltPdfFunc --dotnet-isolated -n net8.0
cd CobaltPdfFunc
dotnet add package CobaltPDF
Terminal
func init CobaltPdfFunc --dotnet-isolated -n net8.0
cd CobaltPdfFunc
dotnet add package CobaltPDF.WebKit

3 Configure the engine

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 = new HostBuilder()
    .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")!);
await CobaltEngine.PreWarmAsync();

host.Run();

Windows plan

Program.cs
using CobaltPdf;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

var host = new HostBuilder()
    .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 = new HostBuilder()
    .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 class RenderPdf
{
    [Function("RenderPdf")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        var html = await new StreamReader(req.Body).ReadToEndAsync();

        var pdf = await new CobaltEngine()
            .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:

Terminal
dotnet publish -c Release -o ./publish
func azure functionapp publish <YOUR_APP_NAME>
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.
Terminal
dotnet publish -c Release -o ./publish
func azure functionapp publish <YOUR_APP_NAME>

Linux plan — custom container (required)

Build an image on the Functions base with Chromium's system libraries, push it to a registry, and create the function app from the image:

Dockerfile
FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot

RUN apt-get update && apt-get install -y --no-install-recommends \
    libglib2.0-0 libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
    libcups2 libdrm2 libxkbcommon0 libatspi2.0-0 libx11-6 \
    libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 \
    libgbm1 libpango-1.0-0 libcairo2 libasound2 \
    fonts-liberation fonts-dejavu-core \
    && rm -rf /var/lib/apt/lists/*

COPY ./publish /home/site/wwwroot
Azure CLI
dotnet publish -c Release -o ./publish

az acr create -n <ACR> -g <RG> --sku Basic --admin-enabled true
az acr login -n <ACR>
docker build -t <ACR>.azurecr.io/pdf-func:1 .
docker push <ACR>.azurecr.io/pdf-func:1

az functionapp create -n <APP> -g <RG> \
  --plan <PREMIUM_PLAN> --storage-account <STORAGE> \
  --functions-version 4 --os-type Linux \
  --image <ACR>.azurecr.io/pdf-func:1 \
  --registry-server https://<ACR>.azurecr.io \
  --registry-username <ACR> \
  --registry-password "$(az acr credential show -n <ACR> --query 'passwords[0].value' -o tsv)"

76 Test

Terminal
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.