DEV Community

IronSoftware
IronSoftware

Posted on

Playwright Memory and Performance Issues (Issue Fixed)

Developers using Playwright for HTML-to-PDF conversion in .NET applications frequently encounter memory growth that accumulates over time, eventually causing out-of-memory errors or degraded performance. The issue stems from Playwright's architecture as a browser automation framework, where each operation spawns a full Chromium browser process consuming 200-400MB or more. This overhead, acceptable for end-to-end testing, becomes problematic for high-volume document generation. This article examines the root causes, documents common performance patterns, and presents an alternative approach using a library purpose-built for PDF operations.

The Problem

Playwright, developed by Microsoft, is an excellent browser automation tool designed primarily for end-to-end testing. While it includes PDF generation capabilities through the page.pdf() method, this functionality leverages the same full browser infrastructure used for testing. Each PDF generation operation requires launching or reusing a complete Chromium instance, including the V8 JavaScript engine, rendering pipeline, and network stack.

This architecture creates several performance challenges for PDF-focused workloads:

  1. Memory overhead: A single Chromium instance consumes 50-150MB at startup, growing to 200-400MB or more depending on page complexity. With high-volume generation, memory accumulates faster than it can be released.

  2. Cold start latency: Browser launch times range from 2-20 seconds depending on the environment, with serverless platforms and containers experiencing the longest delays.

  3. Resource cleanup complexity: Browser contexts, pages, and their associated resources require explicit disposal in the correct order. Missing any step leaves resources orphaned.

  4. Concurrency limitations: Running multiple browser instances simultaneously multiplies memory requirements. Microsoft recommends at least 1GB per concurrent browser.

The problem is particularly severe in long-running services, batch processing jobs, and containerized deployments where PDF generation occurs repeatedly over hours or days. Memory growth is gradual - not a sudden spike, but a slow accumulation that eventually triggers OOM errors or forces container restarts.

Error Messages and Symptoms

Developers encountering Playwright memory and performance issues typically observe these patterns:

System.OutOfMemoryException: Out of memory.
   at Microsoft.Playwright.Page.PdfAsync(PagePdfOptions options)
Enter fullscreen mode Exit fullscreen mode
PlaywrightException: Timeout 30000ms exceeded.
   at Microsoft.Playwright.BrowserType.LaunchAsync(BrowserTypeLaunchOptions options)
Enter fullscreen mode Exit fullscreen mode
PlaywrightException: Target page, context or browser has been closed
Enter fullscreen mode Exit fullscreen mode
JavaScript heap out of memory
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed
Enter fullscreen mode Exit fullscreen mode

Symptoms include:

  • Memory usage climbing steadily with each PDF generation, never returning to baseline
  • Browser launch taking 20-30+ seconds instead of expected 1-4 seconds
  • PDF operations timing out after working successfully for hours
  • Docker containers being killed by OOM killer
  • Kubernetes pods restarting due to memory limits
  • Application becoming unresponsive after processing several hundred documents
  • Multiple orphaned Chromium processes visible in task manager
  • Memory usage jumping to 700MB+ when processing files through the browser

Who Is Affected

This issue impacts any .NET application using Playwright for PDF generation at scale:

Operating Systems: Windows, Linux, and macOS deployments. Docker containers are particularly susceptible due to constrained memory limits. The issue affects both x64 and ARM64 architectures.

Framework Versions: .NET Core 3.1, .NET 6, .NET 7, and .NET 8. The architectural overhead is inherent to the browser-based approach rather than framework-specific.

Use Cases: Invoice generation services, report generation, HTML-to-PDF conversion APIs, document templating systems, certificate generation, and any high-volume PDF workflow where documents are generated on demand.

Environments: Docker containers, Kubernetes clusters, AWS Lambda (with significant cold start impact), Azure Functions, and traditional server deployments. Serverless environments face additional challenges due to cold starts and the 280MB+ Chromium binary size.

Evidence from the Developer Community

Memory management and performance issues with Playwright have been documented extensively across GitHub issues and developer discussions.

Timeline

Date Event Source
2020-10-15 Memory increases when same context is reused GitHub Issue #6319
2022-02-01 Memory leak pattern documented with page refresh GitHub Issue #15400
2023-01-01 Request and response events caused memory leak GitHub Issue #2672
2023-05-01 SetInputFilesAsync allocates 700MB+ for 30MB file GitHub Issue #2629
2023-08-01 GPU memory leak in Playwright 1.36 GitHub Issue #2661
2024-01-01 70% higher memory usage in Playwright 1.40.1 GitHub Issue #28942
2024-06-01 Memory consumption issue in Playwright 1.44.1 GitHub Issue #32459
2024-08-01 .NET hosted services memory leak GitHub Issue #2962
2025-06-01 Chrome for Testing uses 20GB+ per instance GitHub Issue #38489

Community Reports

"If you simply refresh a page every second, it would end up taking over 400MB of memory in about 20 minutes. With more complex operations, node can take up to 1GB of memory in less than 20 minutes of running."
— Developer, GitHub Issue #15400

"The app uses more memory over time (both managed and unmanaged). Most of the memory is retained by StdIOTransport... This is happening on multiple operating systems including Windows 10, Windows 11, WSL2, and Linux."
— Developer, GitHub Issue #2962

"When uploading a ~30MB PDF file, memory usage jumps from about 40MB to >700MB just from the file upload and persists for quite a long time. This has impacted long running scenarios where file uploads are the first dependency, and also affects concurrent runs due to out of memory issues."
— Developer, GitHub Issue #2629

"After upgrading Playwright from version 1.41.1 to 1.44.1, we encountered a substantial increase in memory consumption. When running approximately 1000 tests in a single shard with 8 GB of memory, Playwright fails with a JavaScript heap out-of-memory error."
— Developer, GitHub Issue #32459

In Playwright 1.57, a significant issue emerged where each Chrome for Testing instance uses approximately 20GB of memory, causing system crashes when running multiple workers. This represented a dramatic change from the lightweight open-source Chromium used in earlier versions.

Root Cause Analysis

The memory and performance issues in Playwright stem from its fundamental architecture as a browser automation framework rather than a PDF generation library.

1. Full Browser Process Overhead

Playwright spawns a complete Chromium browser process for PDF generation. This includes:

  • V8 JavaScript engine
  • Blink rendering engine
  • Network stack
  • GPU acceleration subsystem
  • Extension infrastructure

Even in headless mode, Chromium consumes 50-150MB at baseline, growing significantly with page complexity. Peak memory consumption for standard Chromium is approximately 1094MB, dropping to around 690-706MB in headless/minimal configurations.

2. Cold Start Latency

Browser launch introduces significant latency:

  • Local development machines: 1-4 seconds typical
  • CI/CD pipelines: 6-15 seconds on smaller instances
  • Serverless environments: 20-30+ seconds when the Chromium binary must be loaded
  • Docker containers: 2-20 seconds depending on resource allocation

This overhead compounds in high-volume scenarios where browser instances cannot be effectively reused.

3. Browser Context Memory Growth

Even when reusing browser instances (a recommended optimization), contexts and pages accumulate memory over time. Benchmarks show that memory does not return to baseline between operations, requiring periodic browser restarts to reclaim resources.

4. Chromium Binary Size

The Playwright installation downloads multiple browsers totaling over 400MB. For Chromium alone, the binary exceeds 280MB, creating challenges for serverless deployments with function size limits (e.g., Vercel's 50MB limit, Lambda layer constraints).

5. Resource Cleanup Complexity

Proper cleanup requires disposing resources in the correct order:

  • Pages before contexts
  • Contexts before browsers
  • Event handlers must be detached

Missing any step, or disposing in the wrong order, leaves resources orphaned. The async disposal pattern has documented issues where operations hang indefinitely.

Attempted Workarounds

The Playwright community has developed various approaches to mitigate memory and performance issues.

Workaround 1: Browser Instance Reuse

Approach: Launch the browser once and reuse it across multiple PDF generations.

public class PlaywrightPdfService : IDisposable
{
    private IPlaywright _playwright;
    private IBrowser _browser;

    public async Task InitializeAsync()
    {
        _playwright = await Playwright.CreateAsync();
        _browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
        {
            Headless = true,
            Args = new[] { "--no-sandbox", "--disable-dev-shm-usage" }
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await using var context = await _browser.NewContextAsync();
        var page = await context.NewPageAsync();
        await page.SetContentAsync(html);
        var pdf = await page.PdfAsync();
        await page.CloseAsync();
        return pdf;
    }

    public void Dispose()
    {
        _browser?.CloseAsync().Wait();
        _playwright?.Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Memory still accumulates over time within the browser instance
  • Requires periodic browser restart logic
  • Browser crash affects all concurrent operations
  • Complex state management for multi-threaded access

Workaround 2: Periodic Browser Restart

Approach: Track operation count and restart the browser when threshold is reached.

private int _operationCount = 0;
private const int MaxOperationsPerBrowser = 50;
private readonly SemaphoreSlim _lock = new(1, 1);

public async Task<byte[]> GeneratePdfWithRestart(string html)
{
    await _lock.WaitAsync();
    try
    {
        if (_operationCount >= MaxOperationsPerBrowser)
        {
            await _browser.CloseAsync();
            _browser = await _playwright.Chromium.LaunchAsync(_launchOptions);
            _operationCount = 0;
        }
        _operationCount++;
    }
    finally
    {
        _lock.Release();
    }

    // Generate PDF...
}
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Adds latency during restart (2-20+ seconds)
  • Arbitrary threshold may not match actual memory pressure
  • Serialization reduces throughput
  • Restart timing may interrupt concurrent operations

Workaround 3: Limit Concurrency

Approach: Restrict concurrent PDF generation to prevent memory spikes.

private static readonly SemaphoreSlim _semaphore = new(2); // Only 2 concurrent

public async Task<byte[]> GeneratePdfWithLimit(string html)
{
    await _semaphore.WaitAsync();
    try
    {
        // Generate PDF
    }
    finally
    {
        _semaphore.Release();
    }
}
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Does not prevent memory accumulation, only slows it
  • Significantly reduces throughput
  • Queue builds up under load
  • Does not address the root disposal issues

Workaround 4: Docker Process Supervision

Approach: Use Docker's --init flag or process supervisors to handle zombie processes.

FROM mcr.microsoft.com/playwright:v1.48.0-jammy

# Allocate at least 2GB memory (Microsoft recommendation)
# docker run --init --memory=2g your-image

ENTRYPOINT ["dotnet", "YourApp.dll"]
Enter fullscreen mode Exit fullscreen mode

Limitations:

  • Only addresses zombie process cleanup, not memory accumulation within Chrome
  • Requires significant memory allocation per container
  • Does not solve cold start latency
  • Container restarts still occur due to memory growth

Workaround 5: Serverless with External Browser Services

Approach: Use a managed browser service instead of running Chromium locally.

Limitations:

  • Adds external dependency and network latency
  • Additional cost for browser service
  • Potential cold starts on the service side
  • Data leaves your infrastructure

A Different Approach: IronPDF

For teams where PDF generation is the primary use case rather than browser automation, libraries purpose-built for document creation eliminate the browser overhead category of issues entirely. IronPDF embeds a Chrome rendering engine but manages its lifecycle automatically, designed specifically for high-volume PDF operations rather than end-to-end testing.

Why IronPDF Avoids These Issues

IronPDF's architecture differs fundamentally from Playwright in how it approaches PDF generation:

  • Optimized rendering engine: Uses an embedded Chrome engine optimized for document rendering, not full browser functionality
  • Automatic lifecycle management: The rendering engine is started, pooled, and terminated internally without developer intervention
  • No browser instance tracking: Developers do not need to manage browser or page objects
  • Lower memory footprint: Purpose-built for PDF operations without the overhead of extension infrastructure, network stack, and full V8 engine
  • Warm engine reuse: Once initialized, the rendering engine serves subsequent requests without cold start penalty
  • Proper cleanup: Resources are released when PdfDocument objects are disposed, using familiar .NET patterns

The difference is architectural: Playwright exposes full browser automation as a general-purpose API where PDF generation is a secondary feature. IronPDF is purpose-built for PDF operations, using Chrome rendering internally without exposing the complexity or overhead.

Code Example

The following example demonstrates high-volume PDF generation without browser lifecycle management:

using IronPdf;
using System;
using System.Threading.Tasks;
using System.Collections.Concurrent;

public class PdfGenerationService
{
    private readonly ChromePdfRenderer _renderer;

    public PdfGenerationService()
    {
        // Configure once at startup
        // IronPDF manages the rendering engine lifecycle automatically
        Installation.LinuxAndDockerDependenciesAutoConfig = true;

        // Single renderer instance handles all operations
        _renderer = new ChromePdfRenderer();
        _renderer.RenderingOptions.MarginTop = 20;
        _renderer.RenderingOptions.MarginBottom = 20;
        _renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
    }

    public byte[] GeneratePdfFromHtml(string htmlContent)
    {
        // No browser launch - engine already warm
        // No context creation - direct rendering
        using var pdf = _renderer.RenderHtmlAsPdf(htmlContent);
        return pdf.BinaryData;
        // Memory released when using block exits
    }

    public async Task ProcessBatchAsync(string[] htmlDocuments)
    {
        // Process thousands of documents without memory accumulation
        var results = new ConcurrentBag<byte[]>();

        // Parallel processing without browser instance limits
        await Parallel.ForEachAsync(htmlDocuments, async (html, ct) =>
        {
            // Each operation is lightweight - no browser spawn
            using var pdf = _renderer.RenderHtmlAsPdf(html);
            results.Add(pdf.BinaryData);
            // Memory returns to baseline after each document
        });
    }

    public byte[] GenerateInvoice(int invoiceNumber, decimal total)
    {
        string html = $@"
            <!DOCTYPE html>
            <html>
            <head>
                <style>
                    body {{
                        font-family: 'Segoe UI', Arial, sans-serif;
                        padding: 40px;
                        color: #333;
                    }}
                    .header {{
                        border-bottom: 2px solid #0066cc;
                        padding-bottom: 20px;
                        margin-bottom: 30px;
                    }}
                    .invoice-number {{
                        font-size: 24px;
                        color: #666;
                    }}
                    .total {{
                        font-size: 28px;
                        font-weight: bold;
                        color: #0066cc;
                        text-align: right;
                        margin-top: 40px;
                    }}
                    table {{
                        width: 100%;
                        border-collapse: collapse;
                    }}
                    th, td {{
                        padding: 12px;
                        text-align: left;
                        border-bottom: 1px solid #ddd;
                    }}
                </style>
            </head>
            <body>
                <div class='header'>
                    <h1>Invoice</h1>
                    <p class='invoice-number'>INV-{invoiceNumber:D6}</p>
                    <p>Generated: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</p>
                </div>
                <table>
                    <tr><th>Description</th><th>Amount</th></tr>
                    <tr><td>Services Rendered</td><td>${total:N2}</td></tr>
                </table>
                <p class='total'>Total: ${total:N2}</p>
            </body>
            </html>";

        using var pdf = _renderer.RenderHtmlAsPdf(html);
        return pdf.BinaryData;
    }

    public byte[] GenerateFromUrl(string url)
    {
        // Render external URL - JavaScript executes automatically
        using var pdf = _renderer.RenderUrlAsPdf(url);
        return pdf.BinaryData;
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points about this code:

  • No Playwright.CreateAsync() or browser.LaunchAsync() - engine lifecycle is automatic
  • No browser context or page management - direct HTML to PDF conversion
  • Single renderer instance serves unlimited requests without memory growth
  • Standard using blocks release memory predictably
  • Parallel processing works without per-browser memory overhead
  • Same code works on Windows, Linux, and macOS
  • Docker containers work without special memory allocation or process supervisors

Comparison: Playwright vs IronPDF Setup

Playwright approach:

// Install Playwright browsers (400MB+ download)
// playwright install chromium

// Launch browser (2-20+ second startup)
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
    Headless = true,
    Args = new[] { "--no-sandbox", "--disable-dev-shm-usage" }
});

// Create context and page
await using var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();

// Set content and generate PDF
await page.SetContentAsync(html);
var pdfBytes = await page.PdfAsync(new PagePdfOptions
{
    Format = "A4",
    MarginTop = "20px",
    MarginBottom = "20px"
});

// Must dispose page, context, browser, playwright in order
// Memory may not fully release
// Browser restart needed after ~50-100 operations
Enter fullscreen mode Exit fullscreen mode

IronPDF approach:

var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;

using var pdf = renderer.RenderHtmlAsPdf(html);
var pdfBytes = pdf.BinaryData;
// Done - no lifecycle management, no periodic restarts
Enter fullscreen mode Exit fullscreen mode

Memory and Performance Comparison

Metric Playwright IronPDF
Initial memory footprint 50-150MB baseline Lower - optimized engine
Memory per operation Grows with each operation Returns to baseline
Cold start time 2-20+ seconds Subsecond after first init
Browser restart required Every 50-100 operations Never
Concurrent instance memory 1GB+ per browser Shared engine
Docker memory recommendation 2GB minimum Standard allocation

API Reference

For details on the methods used above:

Migration Considerations

Licensing

IronPDF is commercial software with per-developer licensing. A free trial is available for evaluation. Teams should verify that IronPDF meets their requirements before committing to migration, particularly if Playwright was chosen for its open-source licensing.

API Differences

The APIs differ significantly in philosophy:

  • Playwright: General browser automation API with PDF as one capability
  • IronPDF: Purpose-built PDF API using Chrome rendering internally

Migration involves replacing browser lifecycle code with direct PDF generation calls. For applications using Playwright only for PDF generation, this simplifies the codebase significantly. For applications using Playwright for broader browser automation (testing, screenshots, scraping), IronPDF would only replace the PDF portion.

What You Gain

  • Elimination of browser lifecycle management code
  • No memory accumulation requiring periodic restarts
  • Predictable memory behavior without monitoring infrastructure
  • Faster PDF generation (no browser launch overhead)
  • Consistent behavior across Windows, Linux, and macOS
  • Docker containers without special memory allocation
  • Simpler deployment without 400MB browser downloads

What to Consider

  • Commercial licensing cost versus engineering time spent on memory management
  • Migration effort for existing Playwright PDF codebases
  • If using Playwright for non-PDF browser automation, that code remains separate
  • Different rendering engine may produce slightly different output formatting
  • IronPDF has its own learning curve for advanced features

When Playwright Makes Sense

Playwright remains an excellent choice for certain scenarios:

  • End-to-end testing: Its primary design purpose
  • Web scraping and automation: Full browser capabilities
  • Screenshot generation: When combined with PDF
  • Low-volume PDF generation: Occasional documents where overhead is acceptable
  • Existing Playwright investment: When PDF is a small part of a larger Playwright workflow

For dedicated, high-volume PDF generation services, a purpose-built library eliminates an entire category of operational concerns.

Conclusion

Playwright's memory and performance characteristics for PDF generation stem from its architecture as a full browser automation framework. The 200-400MB memory overhead per instance, cold start latency, and resource cleanup complexity create operational challenges for high-volume document generation. For teams where PDF generation is the primary use case, switching to a library designed specifically for PDF operations eliminates these architectural constraints.


Jacob Mellor is CTO at Iron Software and built the core IronPDF codebase over 25+ years of commercial software development.


References

  1. Memory leak question - Issue #15400{:rel="nofollow"} - Memory growth pattern documentation
  2. .NET hosted services memory leak - Issue #2962{:rel="nofollow"} - StdIOTransport memory retention
  3. Memory Consumption Issue with Playwright 1.44.1 - Issue #32459{:rel="nofollow"} - OOM error in testing
  4. 70% Higher memory usage in v1.40.1 - Issue #28942{:rel="nofollow"} - Version regression
  5. Memory increases when same context is used - Issue #6319{:rel="nofollow"} - Context reuse memory growth
  6. SetInputFilesAsync memory allocation - Issue #2629{:rel="nofollow"} - 700MB allocation for file uploads
  7. Chrome for Testing 20GB+ memory - Issue #38489{:rel="nofollow"} - Playwright 1.57 memory spike
  8. Browser launch time inconsistent - Issue #4345{:rel="nofollow"} - Cold start variance
  9. Warm up time nearly 20 seconds - Issue #28707{:rel="nofollow"} - Browser launch delay
  10. How to Generate PDFs with Playwright{:rel="nofollow"} - BrowserStack guide on PDF generation
  11. Running Playwright on AWS Lambda{:rel="nofollow"} - Serverless deployment challenges
  12. Playwright Memory Leak Fixes 2025{:rel="nofollow"} - Community optimization strategies
  13. Scaling Headless Browsers: Contexts vs Instances{:rel="nofollow"} - Memory architecture comparison
  14. Playwright Browser Footprint{:rel="nofollow"} - Memory consumption benchmarks

For IronPDF documentation and tutorials, visit ironpdf.com.

Top comments (0)