The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Use Convex as the control plane, not the browser. Your Convex functions should authenticate a request, record a job, and coordinate its state. Run Playwright in a Node-capable worker or connect to a managed browser service, then write the result back to Convex. This separation avoids trying to launch Chromium inside Convex HTTP actions, which use the Fetch API environment and do not provide Node-specific browser APIs.
The production architecture
A reliable system has four parts:
- Frontend: the user starts an automation and watches its status through Convex.
- Convex: authenticated mutations create jobs; queries expose status; actions or HTTP actions dispatch work and persist results.
- Browser runner: a worker you operate or a remote browser provider runs Playwright.
- Target application: the site your automation is allowed to visit.
The request path should be asynchronous for anything that can exceed a short interactive request: accept the request, create a durable job, execute it in the worker, and update the job to succeeded or failed. Store a useful error message and timestamps so users can retry without losing history.
Why Convex should not launch Chromium
Convex HTTP actions expose HTTP endpoints at your deployment’s .convex.site address. They receive Fetch API Request objects and return Response objects; they can call Convex queries, mutations, and actions. They run in the same managed environment as other Convex functions, however, and do not expose Node-specific APIs. Treat an HTTP action as an ingress and coordination boundary, not as a Chromium host.
HTTP actions also have a documented 20 MB request and response limit, are not automatically retried on errors, and are unnecessary when a caller you control can use a Convex client directly. These constraints favor a job record and a separate runner instead of keeping a browser open during an HTTP request.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Choose where Playwright runs
| Option | What you operate | Important trade-offs |
|---|---|---|
| Own Node worker | A worker image containing the Playwright package, matching browser binaries, and system dependencies. | You control isolation, updates, scaling, and image size. Playwright browser downloads can add hundreds of megabytes; its documentation gives example footprints of 281 MB for Chromium and 187 MB for Firefox. |
| Managed browser | Your application connects to a vendor-hosted browser over a supported protocol. | Less browser maintenance, but verify credentials, session limits, regional availability, pricing, and protocol support for your exact scripts. |
| Self-hosted browser service | A browser endpoint, such as a documented Browserless Docker deployment, plus its networking and monitoring. | You own upgrades, capacity, incident response, and authentication. A reachable endpoint without a token can expose browser-control operations, including an endpoint that can run supplied code. |
Browserless documents Playwright connections by replacing chromium.launch() with chromium.connectOverCDP(). CDP supports most scripts, but Browserless identifies features and browser choices that require Playwright’s native protocol. Confirm compatibility before committing to a remote provider.
Model a durable Convex job
Keep the browser payload small: store a URL, an allow-listed operation, and parameters in Convex, rather than uploading screenshots or arbitrary scripts through an HTTP action.
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const create = mutation({
args: {
url: v.string(),
operation: v.union(v.literal("capture"), v.literal("extract")),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Authentication required");
// Validate the URL and operation against your own policy here.
return await ctx.db.insert("browserJobs", {
userId: identity.subject,
url: args.url,
operation: args.operation,
status: "queued",
createdAt: Date.now(),
});
},
});
export const get = query({
args: { id: v.id("browserJobs") },
handler: async (ctx, args) => {
const job = await ctx.db.get(args.id);
const identity = await ctx.auth.getUserIdentity();
if (!job || !identity || job.userId !== identity.subject) return null;
return job;
},
});
The worker should claim a queued job atomically (for example, changing it to running with a lease), perform the permitted steps, and call a server-side Convex mutation to save the result. Add a lease expiration so a crashed worker does not leave a job running forever. Enforce per-user quotas, destination allow-lists, navigation and total execution timeouts, and maximum output sizes.
Install and run Playwright in your worker
Install a Playwright version and its browsers together so the binary matches the package. The installation command and dependency setup belong in the worker image, not in a Convex function.
npm install playwright
npx playwright install --with-deps chromium
A minimal runner can look like this:
import { chromium } from "playwright";
export async function runCapture(url) {
const browser = await chromium.launch({ headless: true });
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
return await page.screenshot({ fullPage: true, type: "png" });
} finally {
await browser.close();
}
}
In production, do not accept JavaScript from users and evaluate it blindly. Map a small set of named operations to code you reviewed. Close every browser and context in a finally block, cap concurrency, and reject private-network destinations if users can submit arbitrary URLs.
Connect to a managed browser
When a provider supplies a CDP endpoint, keep its token in the worker environment and connect from Node:
Rank #2
import { chromium } from "playwright";
const browser = await chromium.connectOverCDP(
`https://production-sfo.browserless.io?token=${process.env.BROWSER_TOKEN}`
);
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
const title = await page.title();
console.log(title);
} finally {
await browser.close();
}
The hostname and protocol in this example are illustrative of the connection pattern; use the endpoint supplied by your provider. Check whether your script needs native Playwright protocol features before choosing CDP.
Deploy Convex safely
Development, preview, staging, and production
Convex provides a development deployment for each team member and one shared production deployment per project. Use a preview deployment for branch validation. For a longer-lived staging environment, use a separate Convex project rather than treating production as a test database.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesDeploy backend changes with:
npx convex deploy
The CLI typechecks, generates code, bundles functions, and pushes functions, indexes, and schema. In CI, use a deployment key and select the intended production or preview deployment through the CI environment. Your frontend host remains a separate pipeline; configure its Convex client with the production deployment URL only after the backend is ready.
Keep releases backwards compatible
Convex’s production guidance says functions should be backwards compatible. An older website bundle may still call the backend after a deploy, and scheduled functions execute the currently deployed code with the arguments captured when they were scheduled. Add fields rather than abruptly renaming them, accept old argument shapes during a migration, and leave compatibility code in place until queued jobs and old clients have drained.
Store browser credentials as deployment secrets
Convex environment variables are configured per deployment, so development, preview, staging, and production can use different browser-provider credentials. Declare expected variables in convex/convex.config.ts when you want typed access and deploy-time validation. Current documented limits are 512 variables per deployment, 512 KiB total variable name/value capacity, and 8 KiB for one value.
- Put the managed-browser token or self-hosted endpoint credential in trusted Convex or worker configuration.
- Never put that token in a public frontend environment variable or ship it in JavaScript.
- Use separate credentials per environment and rotate them without reusing development tokens in production.
- Use
CONVEX_CLOUD_URLfor Convex clients andCONVEX_SITE_URLwhen configuring HTTP actions, as documented by Convex.
If the worker is outside Convex, give it a narrowly scoped server credential that can update only the jobs it is authorized to process. Authenticate every user request before creating a job; do not expose a browser-control endpoint directly to untrusted callers.
Expose an HTTP endpoint when an external caller needs it
For webhooks or an external queue, an HTTP action can validate a signed request, create a Convex job, and return a small JSON acknowledgment. Keep the response under the 20 MB limit and implement your own retry and idempotency keys because HTTP actions are not automatically retried. A caller you own should normally use the Convex client instead of adding an HTTP layer solely to call a function.
Return a job identifier immediately. Let the frontend subscribe to the job query, or let a webhook report completion. Do not send a full PDF or screenshot through the action when a storage reference or signed download URL will do.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server when your requirement is a clean capture rather than custom Playwright interaction. One GET request returns PNG, JPEG, WebP, or a PDF. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers.
It also supports full-page screenshots with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets or custom viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, request and resource blocking, custom headers/cookies/user agents/Authorization, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed public image links, asynchronous jobs with signed webhooks, bulk capture of 100 URLs per call, a usage API, an OpenAPI specification, and parameter names compatible with other screenshot APIs. Its MCP server provides take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchUse its API key only on trusted server infrastructure. The following call captures Stripe as WebP:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for option names and response behavior. Equivalent clients are:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan, and yearly billing provides two months free. Create a free ScreenshotNeo account to try it.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance, reliability, and cost decisions
- Cold starts and browser launch: keep a worker warm when latency matters, but recycle browsers periodically to contain leaks.
- Concurrency: set a fixed number of browser contexts per worker based on memory, then queue excess jobs instead of allowing unbounded launches.
- Timeouts: use separate navigation, selector-wait, and total-job deadlines. Record which deadline fired.
- Retries: retry transient provider or network failures with an idempotency key; do not repeat non-idempotent user actions without a checkpoint.
- Payloads: store large artifacts in object storage and save metadata or references in Convex. Respect the 20 MB HTTP action limit.
- Cost: measure browser minutes, worker memory, storage, provider sessions, and screenshot volume separately. Provider prices and quotas vary; verify the plan for your workload rather than assuming a published limit.
Troubleshooting
“Cannot find module” or missing browser executable
The worker has the Playwright package but not its matching browser binary or system dependencies. Run the Playwright browser-install command during image build and pin compatible package and browser versions.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Convex function cannot launch Chromium
That is an architectural mismatch: move Playwright to the Node worker or remote browser and let Convex create and track the job.
HTTP action returns a timeout or oversized response
Return an acknowledgment and job ID, then process asynchronously. Save the artifact elsewhere and persist a reference, not the binary, in the action response.
Jobs remain “running” after a crash
Use a lease with an expiration timestamp. A reaper or the next worker pass can move expired leases back to queued with an attempt count.
Old clients fail after deployment
Keep function arguments and scheduled-job data compatible with the previous release. Deploy additive schema and behavior first, migrate callers, then remove compatibility code.
Remote connection is rejected
Check the token, endpoint protocol, browser choice, and whether your script requires native Playwright protocol features that the provider’s CDP path does not implement.
Credentials appear in browser network logs
Move them out of frontend configuration. Store them in per-deployment Convex or worker secrets and proxy the operation through an authenticated server boundary.
Pre-launch checklist
- Playwright and browser binaries are version-matched in the worker image.
- Every job has authentication, authorization, a timeout, a lease, and an idempotency strategy.
- User-supplied URLs and actions pass an allow-list and private-network checks.
- Provider tokens are absent from frontend bundles and separated by deployment.
- Preview or staging has been exercised before the shared production deployment.
- Old clients and scheduled jobs remain compatible during rollout.
- Artifacts, logs, failures, and billing-relevant provider responses are observable without exposing secrets.
FAQ
Can Convex run Playwright directly?
Not as the browser host. Use Convex for coordination and a Node-capable worker or remote browser for Playwright.
Should every automation request use an HTTP action?
No. A controlled frontend should call Convex through its client. Use an HTTP action for external callers, webhooks, or another integration boundary.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Is a separate Convex project required for staging?
It is the documented choice for longer-lived staging; preview deployments are suitable for branch validation.
Can I expose my self-hosted browser endpoint to users?
Do not expose it directly. Put an authenticated, authorized job service in front of it and configure endpoint authentication.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




