ScreenshotNeo

BlogComparisons

Screenshot API vs Google Cloud Run with Playwright for Screenshots

Compare a managed screenshot API with Playwright on Cloud Run, including runnable code, deployment trade-offs, costs, scaling, and how to choose.

By the ScreenshotNeo team4 October 202610 min read

A managed screenshot API is usually the simpler fit when its documented capture options cover your job and you want to avoid deploying a browser. Playwright on Google Cloud Run is a better fit when you need custom browser logic, control over the container and dependencies, or integration with an existing Google Cloud workload. There is no universal cost or speed winner: compare the actual provider’s limits and price with your expected Cloud Run resource use and operating work.

“Screenshot API” does not identify a particular vendor. This guide therefore compares the general managed approach with a self-hosted Playwright service on Cloud Run, and uses Cloudflare Browser Run only as a documented example of a managed browser product. Confirm the chosen provider’s current features, quotas, data handling, regions, and pricing before committing.

1. How the two approaches differ

Question Managed screenshot API Playwright on Cloud Run
What you deploy Typically, your application sends a request to a provider endpoint. The provider’s setup and output flow vary. Your application container, browser dependencies, and Playwright workflow.
Browser control Limited to the provider’s API, unless it also offers browser sessions. Cloudflare, for example, documents both Quick Actions and Playwright-controlled sessions. Your code controls navigation, browser context, waits, interactions, and capture.
Operations Can avoid building and maintaining a browser container for supported jobs. You still need to handle credentials, errors, quotas, and output. You own the container image, browser compatibility, deployment, and workload tuning. Google manages the Cloud Run platform.
Scaling and limits Check request quotas, concurrency, queues, browser time, and output limits in the provider’s current documentation. Cloud Run scales instances, but browser memory, startup time, request concurrency, and timeout settings still need workload validation.
Cost model Depends on the provider: it may charge per request, browser time, concurrency, or a plan allowance. Usage-based Cloud Run charges depend on resources and duration, with potential network and companion-service costs.

Google’s guide to browser and OS automation in Cloud Run identifies screenshots and PDFs as headless Chrome use cases, recommends installing Chromium in the container, and names Playwright and Puppeteer as browser-control libraries. Cloudflare’s Browser Run guide documents simple Quick Actions as well as browser sessions controlled with tools such as Playwright. Those examples describe those products, not every screenshot API.

2. Decide based on the workflow

Choose a managed API when

  • The job is a straightforward URL-to-image or URL-to-PDF request.
  • The provider documents the formats, viewport, waiting behavior, authentication, and delivery options your application needs.
  • You prefer not to package Chromium and browser libraries or maintain their compatibility.
  • The provider’s documented concurrency and quota fit your traffic pattern.

Choose Cloud Run with Playwright when

  • The capture needs custom navigation, interactions, or browser-context setup that an API does not expose.
  • You need control of the container’s dependencies or want to keep the browser workflow alongside an existing Google Cloud application.
  • You can own browser updates, container builds, deployment, and workload monitoring.
  • You need to decide how requests are queued, retried, authenticated, and stored.

For a basic screenshot, compare the provider’s request path with the full Cloud Run setup and operating work. For a complex browser session, compare the provider’s session support and limits with the Playwright code you would operate. These are fit criteria, not benchmark results.

3. Build a screenshot service with Playwright on Cloud Run

The following small service accepts a URL, opens it in headless Chromium, captures a screenshot, and returns PNG bytes. It is a runnable starting point; before exposing it publicly, add your own authentication, URL policy, rate limits, and storage or delivery behavior.

Project files

package.json
{
  "name": "cloud-run-playwright-shot",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": {
    "express": "^4.21.2",
    "playwright": "^1.51.0"
  }
}
server.js
import express from 'express';
import { chromium } from 'playwright';

const app = express();
const port = process.env.PORT || 8080;
const browser = await chromium.launch({ headless: true });

app.get('/shot', async (req, res) => {
  const rawUrl = req.query.url;
  if (typeof rawUrl !== 'string') {
    return res.status(400).json({ error: 'Provide one url query parameter.' });
  }

  let url;
  try {
    url = new URL(rawUrl);
    if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Unsupported protocol');
  } catch {
    return res.status(400).json({ error: 'url must be a valid http or https URL.' });
  }

  let page;
  try {
    page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
    await page.goto(url.toString(), { waitUntil: 'networkidle', timeout: 45000 });
    const png = await page.screenshot({ type: 'png', fullPage: true });
    res.set('Content-Type', 'image/png');
    res.send(png);
  } catch (error) {
    console.error('Screenshot failed:', error);
    if (!res.headersSent) res.status(502).json({ error: 'Page navigation or screenshot failed.' });
  } finally {
    await page?.close();
  }
});

app.listen(port, '0.0.0.0', () => console.log(`Listening on ${port}`));

Playwright’s screenshot documentation describes viewport, element, and full-page screenshots, and PNG, JPEG, and WebP output. It documents target, type, filename, fullPage, and scale options. Full-page capture cannot be combined with a target element.

Containerize Chromium and deploy

Google’s Cloud Run browser automation guide recommends installing Chromium in the container. A container image is one way to package the app and browser dependencies together. Pin dependency versions for repeatable builds, and use the current Google deployment instructions for your selected build and region.

Dockerfile
FROM node:22-bookworm
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
RUN npx playwright install --with-deps chromium
COPY server.js ./
ENV PORT=8080
CMD ["npm", "start"]
# Build and deploy using the gcloud CLI configured for your project.
gcloud builds submit --tag gcr.io/PROJECT_ID/cloud-run-playwright-shot
gcloud run deploy cloud-run-playwright-shot \
  --image gcr.io/PROJECT_ID/cloud-run-playwright-shot \
  --region REGION \
  --allow-unauthenticated \
  --memory 1Gi \
  --timeout 60

Replace PROJECT_ID and REGION with your project and deployment region. The example permits unauthenticated access to keep the invocation short; a production screenshot endpoint should normally enforce access controls. Review the Cloud Run deployment documentation for current deployment options.

Call the service

curl --get 'https://YOUR_CLOUD_RUN_URL/shot' \
  --data-urlencode 'url=https://example.com' \
  --output example.png

4. Tune capture behavior with Playwright

Use options that match the page instead of treating every site as a static document:

  • Viewport: set viewport: { width, height } when creating the page or context. Use a consistent viewport for comparable captures.
  • Full page: use fullPage: true to capture the entire scrollable page. Lazy-loaded content may require scrolling or other page-specific preparation first.
  • Element: locate the element and call locator.screenshot(). Full-page capture and a target element are mutually exclusive.
  • Image format: choose PNG, JPEG, or WebP with the screenshot type option. JPEG can use a quality setting; quality is relevant to lossy output.
  • Scale: use scale: 'css' for CSS-pixel dimensions or scale: 'device' for device-pixel scaling.
  • Navigation wait: load, domcontentloaded, and networkidle represent different readiness conditions. Analytics, ads, and long polling can make network idle unreliable; a page-specific selector or bounded delay may be a better signal.
  • Browser context: configure viewport, locale, timezone, color scheme, cookies, and authentication state on the context when reproducibility or signed-in pages require them.
// Element capture example, inside a handler with an existing page:
const card = page.locator('.product-card').first();
await card.waitFor({ state: 'visible', timeout: 10000 });
const webp = await card.screenshot({ type: 'webp', scale: 'css' });

For deterministic output, wait for a meaningful page condition such as a known heading or content container, and consider disabling animations in the capture workflow. A fixed viewport, locale, timezone, and color scheme also help reduce variation.

5. Add production safeguards

  • Restrict destinations: a public endpoint that fetches arbitrary URLs can be abused to reach internal services. Apply an allowlist or other server-side URL policy, and account for redirects and DNS resolution.
  • Limit work: cap navigation time, image dimensions, page size, concurrency, and request rate. Full-page screenshots of very long pages can consume substantial memory.
  • Handle browser lifecycle: close pages and contexts in a finally block. Reuse a browser process where appropriate, but create isolated contexts for requests so cookies and page state do not leak between users.
  • Use bounded retries: retry transient navigation or platform failures with a small limit and backoff. Do not retry invalid URLs, access-denied responses, or deterministic page errors indefinitely.
  • Choose output delivery: returning image bytes is simple for small synchronous jobs. For large files or asynchronous work, store the result and return a controlled download location or job status.
  • Set concurrency deliberately: more simultaneous pages per instance can improve utilization but also raises memory pressure. Measure your pages and tune Cloud Run concurrency and memory for the workload.
  • Protect secrets: do not put credentials in URLs or logs. Use managed secret storage and pass only the credentials needed for the destination.

6. Estimate cost and operational effort

Cloud Run pricing is usage based and subject to a free tier. Google says usage is rounded to the nearest 100 milliseconds; region and billing configuration affect rates. Outbound data transfer and companion services can also add costs. Check the current Cloud Run pricing page for live rates rather than copying a static estimate.

To estimate your own cost, record expected screenshots, average browser duration, CPU and memory allocation, concurrency, region, minimum-instance settings, network egress, and any build, registry, storage, or queue usage. Include engineering time for browser updates, failure handling, observability, and security controls in the operational comparison.

For a managed provider, use its current pricing model and include request allowances, browser-time charges, concurrency charges, overages, output delivery, and any queue or storage costs. Cloudflare’s pricing documentation describes Browser Sessions charges based on browser hours and concurrent browsers; that is a product-specific example, not a generic API price. Compare like-for-like jobs and do not assume either approach is cheaper from a headline rate.

7. Common problems and fixes

Symptom Likely cause What to do
Container starts locally but Chromium fails in Cloud Run Browser binary or operating-system libraries are missing, or the installed browser does not match the Playwright package. Install Chromium and its dependencies in the image as documented for the selected Playwright version; rebuild and deploy the same image you tested.
Navigation times out on pages that look loaded The page keeps network connections open, or the chosen readiness event does not match the site. Use a suitable navigation event, wait for a meaningful selector, and retain a finite timeout.
Screenshot misses images or below-the-fold content Images load lazily or content appears after interaction or scrolling. Scroll through the page or wait for the required content before capture; verify the page-specific behavior.
Memory exhaustion or terminated request Large full-page images, heavy sites, or too many simultaneous browser pages. Reduce concurrency, limit page height or output size where possible, increase memory based on measured need, and consider asynchronous jobs.
Some destinations return errors while others work Authentication, bot defenses, geo restrictions, or target-site failures. Check the destination response and access requirements. Do not assume a screenshot provider or self-hosted browser can bypass site controls.
Endpoint accepts unsafe URLs Arbitrary URL fetching can expose internal network resources. Validate scheme and hostname, block private and reserved address ranges, and revalidate redirects and resolved addresses.
Output differs between runs Dynamic content, time-dependent rendering, animations, or inconsistent viewport and locale. Set a consistent context, wait on a stable page condition, and disable or await animations where appropriate.

8. Try ScreenshotNeo for a managed screenshot endpoint

If a managed API fits the job, ScreenshotNeo is the first service to try: it removes cookie and consent banners, newsletter popups, and chat widgets before capture, bills only clean shots, and its paid plans start at $5 for 3,000 shots.

One GET request returns an image or PDF. See the ScreenshotNeo API documentation for options and response details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers identify the page verdict and billing status. An MCP server lets AI agents, including Claude and Cursor, take screenshots. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

9. FAQ

Is a screenshot API cheaper than Cloud Run?

There is no general winner. The result depends on the provider’s current plan and your Cloud Run resource time, region, memory and CPU, egress, concurrency, and operating effort. Estimate with your own workload.

Can Playwright capture a single element or a full page?

Yes. Playwright supports element and full-page screenshots, but its screenshot documentation says full-page capture cannot be combined with a target element.

Is Cloudflare Browser Run the unnamed screenshot API in this comparison?

No. It is a documented example of a managed option. The title does not specify a vendor, so evaluate the actual service you plan to use.

When should screenshots run asynchronously?

Use a job queue when captures can outlast a synchronous request, arrive in bursts, or need retries and durable result storage. Keep a synchronous endpoint for short jobs with a predictable response time.