Est.

Playwright on Google Cloud Run

Running Playwright on Cloud Run requires building the container the right way.

Senior Writer · · 18 min read
Cover illustration for “Playwright on Google Cloud Run”
Puppeteer & Playwright · September 10, 2026 · 18 min read · 4,012 words

Cloud Run runs Playwright by spinning up a container with a browser inside it on demand, then killing that container the moment the job finishes. No server to patch, no cluster to babysit, no idle VM racking up charges while it waits for the next test run. That's the whole pitch, and it holds up, provided the container is built the way this piece is about to describe.

The pairing works because the division of labor is clean. Cloud Run handles scaling, isolation, and billing. Playwright handles the browser: launching Chromium, navigating pages, taking screenshots, scraping content, driving an AI agent through a web form. Each does one job, and neither needs to know much about the other. That separation is why the pattern deserves real attention, rather than getting filed under "just another Docker deployment."

The confirmed use cases span more ground than most people expect on first glance: UI testing in CI pipelines, web scraping at scale, performance monitoring, automated screenshot capture for visual regression, and increasingly, automation tasks that need to interact with a real rendered page instead of an API response. That last category is growing fast. A workflow booking a flight or filling out a government form has no API to call. It needs a browser, and it needs that browser to appear and disappear cleanly, session after session, without leaking memory or state between runs.

What this setup isn't matters just as much as what it is. It isn't a persistent Selenium Grid sitting on a fleet of always-on machines, and it isn't a long-lived VM-based runner someone has to patch every month. It also isn't a managed browser SaaS product where someone else controls the container internals. Each of those trades off cost, control, and operational overhead differently. Cloud Run's bet is scale-to-zero plus full container control, and that bet only pays off if the container is built right. Most teams get that part wrong on the first attempt, and that's where the rest of this piece spends its time.

Choosing the right base image and why install-at-build-time is non-negotiable

Start from Microsoft's own image, mcr.microsoft.com/playwright:v1.58.2-noble, published through the Microsoft Artifact Registry, and pin the tag exactly. Version tags move fast (by April 2026, v1.59.x had already shipped), so whatever tag ends up in a Dockerfile should be a deliberate choice, not a leftover default from whenever the project started.

Here's the part that trips people up: the Dockerfile.noble image ships with the browser binaries and the system-level dependencies those browsers need (font libraries, codecs, GTK bits), but not the Playwright package itself. That gets installed separately, on top, during the build.

And it has to happen at build time. Not at runtime, not in an entrypoint script, not lazily on first request. Cloud Run's execution model is stateless: a fresh container gets created from the image, and anything not already baked in either doesn't exist yet or has to be fetched over the network before the rest of the app can run. Running playwright install when a container starts turns every cold start into a browser download, and depending on network conditions that download might not even finish before the request times out. Installing everything at build time swaps that unpredictable runtime cost for a fixed, known image size Cloud Run can pull and start the same way every time. Anyone still calling playwright install from an entrypoint script is trading a five-minute build fix for a recurring runtime failure. That's a bad trade, and it should stop being made.

Two build strategies handle this differently, and picking the wrong one for the job costs either flexibility or bloat. Using the official Microsoft base image directly and installing the Playwright npm package on top gives full access to whichever browser builds Playwright bundles for that version, which matters when tests need exact version parity with a local dev environment. The leaner path uses playwright-core (no bundled browser binaries) paired with @sparticuz/chromium, a Chromium build compressed and tuned for serverless. That combination needs at least 512 MB of memory to run reliably, and it buys a meaningfully smaller image at the cost of some flexibility. For most scraping and screenshot jobs, that trade is worth taking, and honestly, the leaner path should be the default unless there's a specific reason to reach for the heavier one. A test suite chasing a rendering bug that only shows up in one exact Chromium build is that reason, and there the fuller Microsoft image is the right call.

The pattern isn't limited to Node.js. A Python and Flask variant documented by Vaibhav Pawar on Medium in January 2025 follows the same logic on a different stack: start from python:3.10-slim, install system dependencies like wget, curl, gnupg, libatk-bridge2.0-0, libnss3, and libnspr4, then pip install flask playwright, then run python -m playwright install during the build. Same principle, different syntax. Whatever language a team uses, the rule holds: browsers get installed when the image is built, never when the container wakes up.

A couple of details round out a hardened build. Run the container as a non-root user, which is what Cloud Run's security posture expects and which limits the damage if something inside misbehaves. And pin exact versions, both Playwright and the browser builds it installs, at build time. Version drift between the Playwright package and the Chromium build it expects doesn't always throw an obvious error. Sometimes it just breaks quietly, which is worse than a loud failure, since nobody notices until a run silently produces garbage.

The image size problem and how multi-stage builds contain it

A Playwright image built naively on the focal Ubuntu base reached 1.94 GB, according to a report in a Playwright GitHub community issue. That's a lot of bytes to pull every time a container scales from zero, and on Cloud Run, image size contributes to cold start overhead, since more data may need to be pulled before the container even starts initializing.

A multi-stage Dockerfile brings that number down substantially, in some cases under 800 MB for the same functional setup. Skipping multi-stage builds for a Playwright image on Cloud Run isn't a minor inefficiency, it's leaving more than half the image on the table for no reason. The idea is simple once laid out, though easy to miss if the Dockerfile grew organically from a single FROM line instead of being planned from scratch. Stage one is the build stage: install every dependency, run whatever compilation or transpilation steps the setup needs. Stage two starts fresh from a clean base and copies over only what's needed at runtime: the compiled code, the browser binaries, the installed packages. None of the build tools. None of the package manager caches. None of the intermediate layers that made stage one heavy.

Why does this matter more for Cloud Run than for, say, a persistent Kubernetes deployment? Because Cloud Run's scale-to-zero model pulls the image fresh far more often than a workload sitting on always-on infrastructure does. A Kubernetes cluster might pull an image once and reuse the same running pod for days. Cloud Run, especially without min-instances set, might pull that image every time traffic returns after a quiet stretch. Every megabyte in that image is a tax that can be paid more than once as instances scale up and down.

A few smaller tactics compound with the multi-stage approach. Strip unused packages out of the final layer instead of leaving them in because they were convenient during development. Cache dependency layers in CI/CD so rebuilds skip work that hasn't changed. And when using the @sparticuz/chromium path from the previous section, that compressed binary is already smaller than a full Playwright browser install, so it reinforces the multi-stage build rather than duplicating the effort.

Worth being honest about the tradeoff, though. @sparticuz/chromium optimizes for size and cold-start speed at the cost of some customizability. Fine for screenshot capture or scraping jobs where a recent, well-behaved Chromium is good enough. Not fine for a test suite that needs to match a precise browser build to catch a rendering regression that only shows up in one specific Chromium version.

A known Chromium path bug in version 1.48.2 and how to work around it

Sometimes the image builds fine, deploys fine, and still fails at the exact moment the browser tries to launch. That's what happened with Playwright version 1.48.2 on Cloud Run, documented in GitHub issue #33313, reported in October 2024.

The symptom is confusing precisely because the deployment logs look healthy. The build log shows Chromium downloading successfully: version 130.0.6723.31, Playwright build v1140, roughly 164.5 MiB. Everything appears to have worked. Then, at runtime, the container throws an "Executable doesn't exist" error the moment it tries to launch the browser.

The root cause traces back to a mismatch in how the browser installation path is set during the build versus what the container runtime resolves at launch. That value tells Playwright to install browsers into a path relative to the Playwright package itself, rather than a fixed system directory, and on Cloud Run's container runtime, that resolved path isn't where the runtime looks when it goes to launch the executable.

The issue documents two failure cases worth separating, because they look similar but need different fixes. In the first, Chromium installs with no path override at all, and the binary lands somewhere Playwright's runtime lookup never checks. In the second, someone already tried fixing it by setting a path override in the Dockerfile's RUN command during install, but the ENV variable the container sees at runtime doesn't match what was used at build time. Same underlying problem, different way of arriving at it: the path used to install and the path checked at launch disagree.

The confirmed workaround is to set PLAYWRIGHT_BROWSERS_PATH=/root/chromium-browser explicitly, as an environment variable, and make sure that exact value shows up both when the RUN command installs Chromium during the build and when the container's ENV is set for runtime. Matching those two moments, build time and run time, is the entire fix. It's a small detail, but it's the kind that costs an afternoon of debugging when missed, since the failure only shows up after deployment, never during the build itself.

Anyone on a newer tag should still verify that build-time and runtime browser paths agree rather than assuming the issue is ancient history. Pinning to a version already tested against this exact failure mode beats assuming newer automatically means fixed.

Cold starts: what causes them, how long they last, and the levers that reduce them

A cold start happens whenever Cloud Run has scaled an instance count down to zero and a new request shows up. Before that request gets handled, a fresh container has to initialize: pull the image if it isn't cached nearby, start the process, get the app ready to accept traffic. The request that triggered all this just sits and waits through the whole sequence.

On Cloud Run, that wait typically runs somewhere in the 5 to 10 second range. That number is worth sitting with, because a Playwright test that takes 3 or 4 seconds to actually run might now take 12 to 14 seconds end to end, with most of that time spent on infrastructure the test itself has no control over. For one ad hoc scraping job, that's tolerable. For a CI pipeline running hundreds of browser tests, that overhead adds up fast, and unevenly, since only the first request after a quiet period pays the full cold-start tax.

There are levers, though, and they don't rank equally. The single biggest one is --min-instances. Setting --min-instances=1 keeps one instance warm at all times, so the scale-from-zero scenario simply doesn't happen for as long as that floor holds, at the cost of paying for that instance's uptime continuously. For most production Playwright deployments, that trade is worth making, full stop. Skipping it to save a few dollars a month and then eating a 10-second penalty on every CI run is the wrong end of that bargain, and teams that skip it are usually optimizing the wrong line item.

Next is --cpu-boost, Cloud Run's startup CPU boost flag, which temporarily allocates more CPU during the instance initialization window specifically to shorten that startup phase. It's not a replacement for min-instances, it's a complement to it, and production workloads tend to run both together rather than picking one.

Image size comes back into play here too, tying directly back to the multi-stage build discussion above. A smaller image pulls faster, which shortens the cold start no matter what else is configured. And for workloads where more than one request lands on the same warm instance (concurrency greater than 1), keeping a Chromium browser context alive across requests inside that instance avoids relaunching the browser process from scratch every time, itself a meaningfully slow operation.

One more tactic worth naming, because it's a little unusual: scheduled min-instance toggling. Documented by atabak.net in August 2025, some production teams set --min-instances=3 during business hours when traffic is predictable and heavy, then drop to --min-instances=0 after hours using Cloud Scheduler to flip the setting automatically. That's a way of buying the warm-instance benefit only when it's actually needed, instead of paying for it around the clock.

Underneath all of this sits a resource floor that's easy to underestimate. Stable Playwright sessions on Cloud Run generally need at least 2 vCPUs and 2 GiB of RAM per instance. Go below that and the failure mode isn't just slower responses, it's browser instability: crashes, hangs, and inconsistent results that look like flaky tests but are actually a resource-starved container struggling to run a full browser engine.

Sizing memory and concurrency so browser sessions don't exhaust the instance

Memory is where a lot of Playwright deployments quietly go wrong, and it goes wrong in a way dashboards don't always show clearly. A headless browser session might start around 200 MB. After enough page navigations in that same session, though, that number can climb past 1 GB. Chrome doesn't release memory back cleanly as pages come and go, and in a long-running container, orphaned processes from crashed tabs or hung renderers can pile up in the background while the health check still reports green.

Cloud Run's ceiling on memory per instance sits at 32 GiB, which sounds generous, and for most serverless workloads it is. Playwright workloads live much closer to the practical floor than that ceiling suggests, though. Most serverless functions sip memory; a browser engine gulps it. That gap is exactly why Playwright deployments need sizing done deliberately, not left at whatever default a gcloud run deploy command happens to set.

Concurrency is the other half of this, and it behaves differently on Cloud Run than most people expect coming from traditional autoscaling. Cloud Run scales based on the number of simultaneous requests hitting a single instance, not CPU or memory utilization. That distinction changes how a Playwright service should be configured, and getting it backwards is the single most common mistake teams make when they treat this like any other web service.

Guidance from atabak.net (August 2025) draws a clear line between two patterns. A CPU-bound workload, which browser rendering absolutely is, should run something like --concurrency=4 --cpu=4 --memory=8Gi, giving each instance enough headroom to run a handful of browser sessions in parallel without starving any of them. An I/O-bound workload, the kind spending most of its time waiting on a database or an API call rather than doing heavy compute, might run --concurrency=80 --cpu=2 --memory=2Gi. That second configuration would be a disaster for Playwright: 80 simultaneous browser sessions crammed into 2 GiB of memory would exhaust the instance almost immediately. Copying an I/O-bound team's concurrency settings onto a browser automation job is exactly how teams end up debugging mystery crashes for a week, chasing a "flaky test" ghost that's really just an out-of-memory kill in disguise.

Concurrency also turns out to be the main cost lever available, something the next section leans on directly. If a single instance can safely run 10 parallel browser sessions instead of 2, fewer total instances are needed for the same throughput, and those extra concurrent requests share the same CPU and memory allocation at no added charge per request. That's a meaningfully different cost curve than just spinning up more instances to cover the same load.

There's also a quieter reliability benefit buried in Cloud Run's ephemeral design. When Chrome crashes mid-task, which does happen, the process sometimes doesn't terminate cleanly and leaves a zombie behind. On a persistent server, that's how memory leaks build up over weeks. On Cloud Run, containers get destroyed after the job finishes instead of being reused indefinitely, so that orphaned process gets wiped out along with everything else in the container. It doesn't fix the underlying crash, but it does mean the crash doesn't compound over time the way it would on long-lived infrastructure.

A reasonable starting point: 2 vCPUs and 2 GiB of RAM as the floor, adjusted upward based on what actual memory growth looks like for the kind of session being run. A screenshot job that loads one page and exits behaves nothing like a multi-step agentic workflow navigating a dozen pages in sequence, and the memory profile needs to be measured for each one directly, not assumed from the other.

Locking down the deployment with OIDC authentication and Zero Trust identity

Playwright runners in production are usually hitting something sensitive: internal staging environments, authenticated web apps, admin panels being tested end to end. The default posture for the Cloud Run service itself should be --no-allow-unauthenticated, full stop, restricting the service to callers that can prove who they are. Leaving it open because it's faster to set up during development is the kind of shortcut that turns into an incident report later, and there's no good reason to ship that shortcut to production even temporarily.

Cloud Run's OIDC model handles that proof cleanly. Cloud Run issues ID tokens asserting caller identity, and the receiving service checks those tokens against Google's public keys. Compared to something like a static API key, that buys proof of identity and intent rather than just proof of holding a string that happened to leak into a config file somewhere. A stolen API key stays a stolen API key forever, until someone remembers to rotate it. A misused OIDC token has a lifespan and a traceable identity behind it.

For CI systems, Workload Identity Federation extends that same model outward. A GitHub Actions pipeline, for instance, can mint OIDC tokens that Google Cloud trusts directly, without ever storing a long-lived service account key inside the CI system's secrets. That removes an entire category of credential leakage risk (a static key sitting in a CI secrets store is exactly the kind of thing that ends up in a breach report) while keeping a full audit trail of which pipeline run authenticated as what.

Secrets belong in Secret Manager, mounted at runtime, never baked into the image layers. That's not just a security nicety, it changes how debugging works too: if a run fails, the exact same container spec can be pulled and replayed locally, because nothing environment-specific is welded into the image. That's genuinely useful when trying to reproduce a bug that only showed up inside the Cloud Run environment.

For Playwright services that are web-accessible rather than purely internal, Identity-Aware Proxy adds another layer, using OAuth 2.0 and OpenID Connect to authorize access based on who the user is and the context they're connecting from, instead of relying only on network-level restrictions. That gives security teams one central place to manage who can reach a given Cloud Run endpoint, rather than scattering access control logic across a dozen individual services.

For organizations with heavier security needs, IAP extends further into BeyondCorp Enterprise, layering device context in alongside user identity, so access decisions account for both who's asking and what they're asking from.

A short checklist ties the container-level hardening together with the identity layer: pin exact Playwright and browser versions at build time, run as a non-root user, keep static credentials entirely out of the image, and scope CI service accounts to be short-lived and specific to each run rather than long-lived and broadly permissioned.

Worth naming, given how much of the earlier discussion touched on AI agents: automated browser agents triggering these Cloud Run jobs benefit from this identity model just as much as human-triggered CI runs do. Identity verification happens automatically on every invocation regardless of who or what kicked it off, which matters more every quarter as more of these triggers come from autonomous systems instead of a person clicking "run" on a pipeline.

What Cloud Run actually costs for Playwright workloads and where the levers are

Cloud Run's pricing, per Google's 2025 pricing documentation, breaks down across a few dimensions. CPU bills at $0.00002400 per vCPU-second. Requests beyond the first 2 million per month cost $0.40 per additional million in a Tier 1 region. There's a free tier covering 180,000 vCPU-seconds and 360,000 GiB-seconds a month, and critically, no charge at all for idle time, unless min-instances is set above zero, in which case that warm capacity gets billed continuously regardless of traffic.

Google's own pricing page offers a useful reference point for what a lightweight service costs: a service in europe-west1 handling 10 million requests a month, averaging 200ms latency, running at 0.167 vCPU and 256 MiB of memory with 1 max concurrent request per instance, comes out to roughly $7.25 a month. That's the baseline for a typical lightweight serverless workload, the kind Cloud Run was originally built around.

Playwright workloads sit nowhere near that baseline, and pretending otherwise just sets teams up for a surprise invoice. Multi-second execution times per run, multiple vCPUs per instance, memory measured in gigabytes rather than megabytes: every one of those pushes cost upward relative to the reference benchmark above. That's not a flaw in Cloud Run's pricing model. Running a full browser engine simply costs more than returning JSON in 50 milliseconds, and no pricing tier changes that arithmetic.

The levers for controlling that cost line up closely with points made earlier in this piece, and that's not a coincidence worth glossing over: the same decisions that improve reliability tend to improve cost too. Higher concurrency per instance means fewer total instances covering the same workload, and since CPU and memory are shared across those concurrent requests at no per-request premium, getting concurrency right (per the sizing guidance a couple sections back) is probably the single highest-leverage cost decision on the table. Active container time, the sum of startup, request processing, and shutdown, is the other lever. Faster test execution and leaner images (tracing straight back to the multi-stage build discussion) directly cut the billed duration of every run.

Committed Use Discounts apply at the billing-account level for Cloud Run compute, and they matter for any team running continuous testing pipelines with predictable, sustained usage rather than sporadic bursts. Worth flagging, though: those discounts don't extend to GPU resources or networking costs, which matters for any team layering GPU-accelerated rendering into their browser automation setup.

And the scheduled min-instance toggling described in the cold start section deserves a second mention here, because it's really a cost tactic wearing a performance-tuning hat. Turning a fixed always-on cost into a time-bounded one, warm during business hours, scaled to zero overnight, captures Cloud Run's scale-to-zero advantage without giving up daytime responsiveness.

That scale-to-zero default, more broadly, is the real structural advantage Cloud Run holds over a persistent VM-based test runner. No idle billing between CI runs, none overnight, none on weekends when nobody's pushing code. For workloads that are naturally bursty, and CI-triggered browser testing usually is, that difference compounds over a billing cycle in a way that's easy to underestimate until the invoices sit side by side.

Deploying across multiple regions for latency-sensitive and high-availability Playwright workloads

Google Cloud's infrastructure spans 43 regions and 130 zones, with its fiber network connecting more than 200 countries. That's a large

Sources

  1. [Bug]: Google Cloud Run deployment not working · Issue #33313 · microsoft/playwright
  2. Running Playwright Tests in Python with Flask on Cloud Run | by Vaibhav Pawar | Medium
  3. cloud.google.com
  4. docs.cloud.google.com

More in Puppeteer & Playwright