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.
Stock Linux plans now work (v1.6.2+). CobaltPDF 1.6.2 bundles Chromium's system libraries and loads them automatically, so the Chromium edition deploys to a stock Linux Functions plan with a normal code deploy — no custom container, no apt. Windows plans also deploy as plain code. (On versions before 1.6.2, Linux required a custom container.)

1 Choose a plan

Consumption Plan is not supported by either engine. It aggressively recycles instances and can't keep the browser pool warm, so renders are slow and unreliable. 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 (v1.6.2+) — any stock Dedicated (B1+) or Premium plan with Always On, deployed as plain code (the package ships Chromium's libraries). We recommend B2 (3.5 GB) as the minimum — Chromium peaks higher than the WebKit edition, and a 1.75 GB B1 can OOM on heavy or image-rich pages. A custom container (step 5) remains an option if you'd rather bake everything into one image.

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.

Code deploy — Windows, and Linux on v1.6.2+

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>
Deploying to a Linux plan? Publish from Linux or CI — not from a Windows machine. A Windows toolchain (and func publish from Windows) zips Chromium's node/chrome binaries without the Unix execute bit, and the read-only package mount can't restore it — so the function fails at render time with node: Permission denied. Deploy from a Linux build agent / GitHub Actions / WSL (which preserve the execute bit), or use the custom-container option below. (Windows-plan deploys and the WebKit edition are unaffected.)

Linux — custom container (optional)

Not required on v1.6.2+ — the code deploy above works on a stock Linux plan. Use a container only if you prefer to bake everything into one image, or on versions before 1.6.2. 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're on a version before 1.6.2. Upgrade to CobaltPDF 1.6.2 or later — it ships Chromium's system libraries and loads them automatically, so a stock Linux plan works with a plain code deploy. (On older versions, deploy as a custom container in step 5, or use .)

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.