Below are solutions to the most frequently encountered issues when using CobaltPDF. If your problem isn't listed here, check the documentation or contact us at support@cobaltpdf.com or open an issue on GitHub.

Chromium requires several system libraries that are not installed by default in minimal Linux images or Docker containers.

Solution: Use the built-in cloud environment preset to configure the engine for your platform:

CobaltEngine.Configure(CloudEnvironment.ConfigureForDocker);

If you're building a custom Docker image, ensure these packages are installed:

RUN apt-get update && apt-get install -y \
    libnss3 libatk1.0-0 libatk-bridge2.0-0 \
    libcups2 libdrm2 libxkbcommon0 libxcomposite1 \
    libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 \
    libcairo2 libasound2 libxshmfence1 fonts-liberation

For Azure App Service, use CloudEnvironment.ConfigureForAzure instead.

A blank or incomplete PDF usually means the page hadn't finished rendering when CobaltPDF captured it. This is common with JavaScript-heavy pages (React, Vue, Angular).

Solution: Use a wait strategy to delay capture until the page is ready:

// Wait for the network to be idle
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.DefaultNetworkIdle)
    .RenderUrlAsPdfAsync(url);

// Or wait for a specific DOM element
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForSelector("#main-content"))
    .RenderUrlAsPdfAsync(url);

For SPAs, call window.cobaltNotifyRender() from your front-end code when the app is fully loaded, and use .WithWaitStrategy(WaitOptions.ForSignal()) on the engine.

Fonts appear as fallbacks (e.g. Times New Roman) when the required font files are not available on the server.

Solutions:

  • Web fonts: Use Google Fonts or other CDN-hosted fonts in your HTML. These are downloaded by Chromium at render time.
  • Local fonts: Install the font files on the server, or use .WithFonts("path/to/fonts") to register them before rendering.
  • Docker: Include font files in your Docker image and install them with fc-cache -f -v.

If you're using custom web fonts, ensure .WithWaitStrategy(WaitOptions.DefaultNetworkIdle) is enabled so Chromium finishes downloading them before capture.

Chromium processes can accumulate memory over many renders. CobaltPDF mitigates this by recycling browser instances after a configurable number of renders.

Solution: Apply the low-memory preset, cap the pool size, and lower the recycle threshold:

builder.Services.AddCobaltPdf(options =>
{
    CloudEnvironment.ConfigureForLowMemory(options);
    options.MaxSize = 2;               // fewer concurrent browsers
    options.MaxUsesPerBrowser = 25;    // recycle after 25 renders
});

The ConfigureForLowMemory preset disables GPU compositing and reduces shared memory requirements. On v1.6.4+, you can also set options.IdleShrinkAfter = TimeSpan.Zero; to release surplus browsers immediately after each render instead of keeping them warm for 60 seconds.

Render time is governed by the configured wait strategy. The default (network idle) waits up to 30 seconds for the page to go quiet — and pages that load slow or hanging external resources (blocked fonts, analytics, or CDN assets on servers with restricted outbound access) burn that timeout in full. This is the most common cause of renders that are fast locally but take 16–45 seconds in production.

Solution (v1.6.4+): Use an explicit wait strategy — it runs alone, with no implicit waits in front of it — or cap the internal network-idle waits:

// Explicit strategy: capture as soon as the report is visible
var pdf = await renderer
    .WithWaitStrategy(WaitOptions.ForSelector("#report", TimeSpan.FromSeconds(60)))
    .RenderUrlAsPdfAsync(url);

// Or keep network idle, but give it at most 5 seconds
var pdf2 = await renderer
    .WithNetworkIdleTimeout(TimeSpan.FromSeconds(5))
    .RenderUrlAsPdfAsync(url);

The timeouts on ForSelector, ForJavaScript, and ForSignal are configurable per render, so genuinely slow pages can be given more time (e.g. TimeSpan.FromSeconds(60)) without slowing everything else down.

Tip: Enable logging (CobaltEngine.LoggingMode or OnBrowserLog) — v1.6.4+ emits [Browser TIMING] entries per render stage, showing exactly where the time goes. And pass a CancellationToken to allow callers to cancel long-running renders gracefully.

Renders that take a couple of seconds on a development machine but 10+ seconds in production almost always come down to the server environment, not the documents. Start by enabling the per-stage timing logs (v1.6.4+) so every second is accounted for:

CobaltEngine.LoggingMode = BrowserLoggingMode.Console;
// Each render then logs [Browser TIMING] lines per stage —
// navigation, waits, PDF capture, and the total.

Then work through the usual causes, in order of likelihood:

  • Antivirus real-time scanning. Every render starts Chromium processes that load a ~286 MB chrome.dll, and real-time scanning of each start adds seconds and causes large run-to-run swings — even on identical documents. Ask your server admin to exclude the Chromium folder from real-time scanning: it's runtimes\win-x64\native under your deployed application. To log the exact path on your server:
    logger.LogInformation("Chromium folder: {Path}",
        System.IO.Path.GetDirectoryName(Chromium.Path) ?? "not found");
    Exclude the whole folder, not just chrome.exe. This is the most common cause of slow renders on Windows Server.
  • Cold browser starts. If the "acquire pooled browser" timing line shows multi-second values, browsers are being launched cold. Set PoolOptions.MinSize to your typical concurrent render count (browsers at MinSize stay warm permanently), and set the IIS app pool to AlwaysRunning with its idle timeout at 0 so the warm pool survives quiet periods.
  • Restricted outbound network. If your servers can't reach external hosts, requests for fonts, analytics, or CDN assets hang rather than fail — burning out network-idle waits. Set an explicit wait strategy (ForSelector, ForDelay, ForSignal) so your strategy is the only wait that runs, or cap the internal waits with WithNetworkIdleTimeout (v1.6.4+).
  • Image compression on heavy documents. The "pdf capture" stage includes an image-compression pass (on by default). On image-heavy documents, try .WithCompression(false) and compare — the trade-off is larger files for faster renders.
  • Server resources. Chromium rendering is CPU-bound. For concurrent rendering we recommend at least 4 vCPUs / 8 GB RAM on SSD storage. Avoid burstable VM tiers (Azure B-series, AWS T-series) — when CPU credits run out, the VM throttles and identical renders swing wildly in duration.
  • Slow page loads. If the "navigate" timing line is large, that's the browser loading your page — any browser on the server would take just as long. If the HTML comes from your own application, pass it to RenderHtmlAsPdfAsync instead of rendering by URL to skip the fetch entirely.

Headers and footers require sufficient page margins to be visible. If margins are too small, Chromium clips the header/footer content.

Solution: Set explicit top and bottom margins that are large enough to contain your header/footer HTML:

var pdf = await renderer
    .WithHeader(headerHtml)
    .WithFooter(footerHtml)
    .WithMargins(top: "30mm", bottom: "25mm")
    .RenderUrlAsPdfAsync(url);

Header/footer templates are rendered in an isolated context with a fixed height. Use inline styles only — external stylesheets are not loaded in the header/footer context.

By default, Chromium does not print background colors and images when generating a PDF. This is the same behavior as the browser's print dialog.

Solution: Enable background printing:

var pdf = await renderer
    .WithPrintBackground()
    .RenderUrlAsPdfAsync(url);

If specific elements are hidden in print, check for @media print rules in your CSS that may be hiding them. You can also use .WithMediaType(CssMediaType.Screen) to force screen styles instead of print styles.

This can occur when PreWarmAsync() is called before the engine has fully initialised, or when the engine is disposed during pre-warming (e.g. during application shutdown).

Solution: Ensure Configure() completes before calling PreWarmAsync(). In ASP.NET Core, call it after Build():

var app = builder.Build();

// Configure first, then pre-warm
CobaltEngine.Configure(CloudEnvironment.ConfigureForDocker);
await app.Services
    .GetRequiredService<CobaltEngine>()
    .PreWarmAsync();

app.Run();

If the error occurs during shutdown, it can safely be caught and ignored — the browser processes are cleaned up regardless.