Playwright in the cloud: CDP connectivity, persistent sessions, and concurrent job management

Playwright in the cloud means running a browser on a remote node that the script controls over a network connection. This approach is useful for parallel checks, long sessions with authorization, and tasks that lack the resources of a work laptop. In practice, consider network latency, profile lifecycle, and a separate limit of concurrent browsers.
Playwright itself remains a client library: it opens pages, works with locators, waits for elements to be ready, and collects artifacts. If you need to choose a framework first, see our comparison of Playwright, Puppeteer and Selenium. Here we will consider more specific questions: where to launch Chromium, how to connect to it through CDP and how to isolate the state of parallel tasks.
In architecture, it is important to separate script execution from state retention. The script container can be re-created, and cookies, localStorage, and IndexedDB often need to be saved after a restart, and proxy settings and browser startup options need to be restored separately. If these two layers are not separated, the failure of one of the nodes can cause the loss of authorization or the re-execution of the business operation.
When to choose local launch, own server or managed browser
Local startup is suitable for development and short tasks, a dedicated server gives control over the environment, and a managed browser reduces the workload of infrastructure maintenance. The choice depends not on the popularity of a particular solution, but on session duration, peak parallelism, and who is responsible for Chromium after a crash.
For local mode, the main limitations are CPU, RAM and the number of browser processes. Four parallel tasks can run stably on a directory page and suddenly run out of memory on a resource-intensive SPA. Before moving to the cloud, check how sites detect headless automation, because another server does not fix signs of browser automation, bad script behavior, or unstable locators.
A dedicated server or container pool is appropriate if the team needs a fixed browser version, private network, disk control, and predictable cost per constant load. But along with monitoring comes image updates, dead process termination, node preparation, log collection, and crash recovery. This is a separate system, not just npm install on a virtual machine.
Managed headless browser convenient when the load is uneven. The script gets the WebSocket endpoint, does the work, and terminates the session. For this, the team pays not only with money, but also with limited access to launch options, tariff restrictions and dependence on the network route to the browser's location region. Before choosing a provider, check support for persistent profiles, file uploads, video recording, proxies, and required Chromium settings: these capabilities differ between services.
| Criterion | Local launch | Own server | Managed browser |
|---|---|---|---|
| start of development | the fastest | infrastructure is needed | API integration required |
| Chromium control | full | full | depends on the provider |
| permanent profile | on the local disk | on a node volume or disk | according to the rules of the service |
| peak parallelism | limited by the machine | limited by the pool | limited by tariff and quota |
| operational support | manual | on the team | partly on the provider |
| command delay | minimal | depends on the topology | depends on RTT to region |
The rule of thumb is simple:
- if the load is consistently high, the own pool often makes better use of resources
- if jobs arrive irregularly, a managed model may be cheaper than idle nodes
- leave local mode for debugging, small internal scripts, and basic performance testing
Before making a decision, describe a typical work scenario. For example, a command has the following conditions:
- checks 200 offices every ten minutes
- one check lasts 40 seconds
- simultaneous login to one profile is prohibited
The average load here is misleading: all tasks arrive at the same time, so the architecture is determined by the peak load on the queue.
For such a process, count how many runs must complete before the next packet, and only then determine the number of slots required.
| Indicator | Meaning |
|---|---|
| Number of checks | 200 |
| Duration of one check | 40 seconds |
| Total browsing time under ideal conditions | 8,000 seconds |
| Time for the next package | 600 seconds |
| Theoretical minimum of parallel slots | 14 (rounding up 8,000 / 600) |
In practice, a buffer is needed for launching browsers, delays and retries; the restriction on simultaneous access to one profile remains in effect.
Also consider where the data is located:
- if the page works with an internal staging environment, the remote browser must have a route to it
- if the result contains personal data, agree on the region of the browser node, the storage period of traces (trace) and access to video recordings
The cloud changes not only the scale, but also the limits of data access.
How are CDP connections and the Playwright protocol different?
A CDP connection connects Playwright to an already running Chromium, and Playwright's native protocol connects to a browser running via BrowserType.launchServer. These connection methods are not interchangeable. The official documentation clearly states that connectOverCDP() provides less complete integration and only supports Chromium-based browsers.
Through browserType.connect() the Playwright client and server must have the same major and minor versions. This option provides fuller support for Playwright capabilities, but requires a Playwright server on the remote side. CDP is more flexible for an already running Chrome or an antidetect profile: an HTTP or WebSocket endpoint is sufficient.
U connectOverCDP() take the existing default context through browser.contexts()[0]. Creating a new context may result in using an empty repository without the required authorization. This is a typical error: the connection is successful, the page opens, but the persistent profile cookies are not available in the new context.

After connecting, check the support of the required capabilities (capability probe): open the test page, read the text with the locator, create a screenshot, check access to the available tabs and correct completion of the connection.
If your script requires it, test interception and routing of requests, file uploads, and trace recording separately. For each check, record the result or error: successful page.goto() not enough to evaluate all connectivity options.
Note that the remote Chromium launch options may differ from those normally set by Playwright.
| A sign | browserType.connect() | chromium.connectOverCDP() |
|---|---|---|
| remote component | Playwright BrowserServer | Chromium with CDP endpoint |
| browsers | Chromium, Firefox, WebKit | Chromium-based only |
| completeness of integration | higher | lower than Playwright's warning |
| versioning requirement | coincidence of major and minor | compatibility check in practice |
| access to permanent default context | depends on the launch | through browser.contexts()[0] |
| typical scenario | remote test runner | cloud browser or ready-made profile |
Network latency affects the execution time of each individual command. The locator, reading the text, and getting the attribute can create multiple rounds of data exchange between the client and the browser. With a large RTT, it is often more profitable to collect data in a single call page.evaluate() or locator.evaluateAll(), than to perform dozens of independent readings.
For simple text assembly, also check locator.allTextContents(). But don't transfer all the business logic to the browser: the code in the page context is more difficult to type, test and diagnose.
Check the difference on the page you are working with. Compare the two methods:
- the first reads 20 cards with separate locators
- the second returns an array of one
evaluate()
Run a series of tests with local and remote connections. If the local time is almost the same, and the remote time differs by several times, the likely cause of the slowdown is the number of network exchanges (round trips), and not the page calculation.
Don't confuse CDP connection performance with site responsiveness. A 403, CAPTCHA, or empty list may be the expected site response, although WebSocket and all Playwright commands work without errors.
Track connection status, browser status, and job progress separately. A successful heartbeat check does not mean that the task has completed with the expected result.
How to save session state and separate browser profiles
A persistent session is created by a profile or saved browser state, not by CDP itself. WebSocket only provides a control channel. After it is disconnected, the browser may continue, terminate, or wait for a reconnection, depending on the infrastructure.
For E2E tests, storageState is often sufficient: Playwright stores cookies, localStorage and, as a separate option, IndexedDB. Standard storageState does not cover sessionStorage, so sites that rely on it require a separate recovery mechanism. The authentication file contains sensitive data, so add its directory to .gitignore, limit access in CI and update test accounts regularly. Do not put this JSON in a Docker image.
For a long-lasting session in which you want to store the full state of the browser, use a persistent user data directory or a profile managed by the browser platform. If cookies, localStorage and, if necessary, IndexedDB are sufficient, it is possible to save and restore periodically storageState.
Cookies, site settings and other Chromium data are stored in the permanent profile directory. A single profile directory cannot be opened by multiple independent processes at the same time without proper coordination: file locks and contention write quickly corrupt the state.

Follow the single writer principle: one active task owns the profile until it completes its work or transfers the access right (lease). Other tasks are waiting in the queue. To read different pages of the same site, it is better to create separate profiles or test accounts than to run competing transitions in the same tab.
Separate three levels of state. This will help avoid confusion:
- the state of the task saves
jobId, the current step, the number of attempts and the business result - the connection state stores the endpoint, the time of the last heartbeat and the reason for the disconnection
- profile status includes cookies and browser storage; keep proxy settings and launch options in a separate session configuration unless the platform manages them itself
When a client crashes, first check that the browser and profile remain active. If so, try reconnecting to the current endpoint or get the current endpoint via the service API. If the browser is closed, restore the profile according to the rules of the platform.
Don't repeat the entire business operation automatically. First, check the checkpoint: whether the order was created, whether the form was sent, whether the result was recorded. For data modification actions, use an idempotency key where the API supports it, or your own log of steps taken. Otherwise, the seemingly safe retry will create a duplicate.
The lifetime of the profile should also be explicitly fixed:
- the one-time profile is deleted after the task
- the test can live until the end of the CI run
- an authorized work profile requires a backup login procedure
Saved cookies do not cancel the server token's validity period, MFA and the site's decision to end the session. The presence of cookies or IndexedDB in the storage does not guarantee that the server will accept them for authorization.
Keep track of profile status versions. The procedure looks like this:
- write down before starting
state_version - after regular completion, increase it together with the control hash of the required data
- if two tasks try to complete the same lease, the second task will detect the version change and will not overwrite the current cookies with an outdated snapshot
This is an example of optimistic locking for a browser profile. If the lease is expired, add a valid owner check and a monotone fencing token: a task that has lost the lease should not be able to write state even after the connection is restored.
For manual intervention, provide a separate profile management mode (ownership):
- the operator receives temporary access to the profile
- the automatic queue stops assigning him tasks
- all actions are identified by a single incident ID
- after the profile is returned, authorization and network parameters are checked
Without such a protocol, a human and a robot can easily start managing the same tab at the same time.
| data | Experiencing reconnect | Experience restarting the browser | Where to control |
|---|---|---|---|
| cookies | usually yes | so in a permanent profile; through storageState only after the state is clearly restored | profile and site policy |
| localStorage | yes | yes in the profile; through storageState after clear restoration | origin and profile |
| sessionStorage | while the tab is alive | not included in storageState; the tab can be restored by the browser | tab and session recovery mechanism |
| IndexedDB | yes | so in a permanent profile; through storageState with indexedDB: true after clear restoration | origin and profile |
| open tabs | while the process lives | can be restored by Chromium settings, but the Playwright execution state and CDP sessions are not automatically restored | browser process and recovery mechanism |
| CDP session id | don't rely on the old ID | don't rely on the old ID | creating a new CDP session |
Consistency testing must be measurable. Write down the control cookie, localStorage value, and URL, disconnect the client, reconnect, and check all three values. Then restart the browser and repeat the test. So you will separately check reconnect and real profile recovery.
How to measure parallelism, timeouts and resource consumption
Concurrency, latency, and resource consumption must be analyzed together, as queue acceleration can simultaneously increase load and the proportion of unstable starts. Only a configuration that produces the predicted result within the agreed time and budget is useful.
Parallelism and load tests
The effectiveness of parallel execution in Playwright should be measured by the number of successful tasks per minute, not by the number of open browsers. Ten concurrent sessions do no good if half are timed out and the rest are waiting for CPU. Start with a small limit and increase it gradually.
In Playwright Test, each worker is a separate process that can run multiple tests. The browser and contexts are created according to the configuration and life cycle of the tests: worker should not be identified with a separate browser context. This model is convenient for independent tests. Application automation often requires its own queue, semaphore, and explicit communication jobId -> profileId -> browser session. Do not confuse the worker runner with the quota of the cloud provider: four workers can rely on the tariff with two parallel sessions.

Perform basic performance testing on the same scenario. The process must be reproducible:
- run 20 tasks with concurrency 1 and commit p50 and p95
- repeat the series with concurrency 2, 4 and 8
- measure the creation time of browser, CDP connect, navigation and business step separately
- record the peak RSS of the client and browser nodes
- calculate the proportion of successful executions without retries and after allowable retries
- stop when p95 spikes or errors exceed an agreed threshold
After testing, analyze the change in indicators. If the throughput barely increases after the fourth session is launched, subsequent launches only increase the queue within the CPU or network. If the CDP connection time increases, check the browser pool and cold start. If only navigation slows down, the cause may be in the proxy, region, or target site.
Timeouts and retries
Divide timeouts by class:
| Timeout class | Purpose |
|---|---|
| Connection timeout | limits waiting endpoint |
| Navigation timeout | refers to the transition |
| Action timeout | limits the time to perform an action with an element |
| Job deadline | completes the entire task |
One global timeout hides the reason: the operator only sees "60 seconds", even though the browser has been waiting for a session for 55 of them.
Set a general deadline, taking into account the time for individual steps. If the entire task is to be completed in 120 seconds, do not assign each of the ten actions 30 seconds independently.
Before each step:
- calculate the budget balance
- compare the local timeout with the remaining time
- choose the smaller of the two values
Then, hanging on one of the initial steps will not cause the script to run for a few more minutes after the result is no longer relevant.
For retries, mark errors as retryable and terminal:
| Error type | Examples | Possibility of repetition |
|---|---|---|
| Retryable | WebSocket break, temporary ECONNRESET, the slot is occupied | sometimes allow reconnection |
| Terminal | invalid selector, authorization denied, business validation | will not disappear after the next repetition of the same scenario |
Use exponential backoff with jitter for transient errors when retry is allowed and safe: for example, after a connection drop, service overload, or HTTP 429 response with Retry-After. Check its result and idempotence before re-executing the operation with the change of data.
Cost, reliability and telemetry
Also estimate the cost by the result. The formula total costs / successful tasks includes:
- browser hours
- proxy traffic
- client code execution
- storage of artifacts
- repeat
A cheap tariff with a low success rate may cost more than a dedicated node.
Determine the acceptable level of errors (error budget) for the infrastructure. For example:
- transport failure can be no more than 0.5% of launches
- p95 CDP connect must stay below five seconds in the working region
These are not universal norms, but an example of a contract. Determine the thresholds after a baseline test and review after changing the Chromium version, container image, or proxy route.

Collect artifacts selectively. Trace quickly fills storage on each successful run and may contain a DOM with sensitive data. Practical mode saves a short technical log always, a screenshot after an error and a full trace only for sampling or repeated failures. The retention policy should provide for automatic deletion of files.
For basic telemetry, the following indicators are sufficient:
job_idandprofile_idfor event communicationsession_create_msandcdp_connect_msfor infrastructure latencynavigation_msandbusiness_step_msfor the scriptattemptanderror_classfor repeat analysisbytes_in,bytes_outandartifact_bytesfor network costssession_stop_statusto detect browsers that have not completed their work
Do not log the full CDP URL, cookies or API key. Endpoint often includes a credential in the query string or gives direct access to the local browser. Mask secrets before a bug gets into a centralized monitoring system.
Universal example of a remote CDP connection
For a managed cloud browser, an endpoint provided by the provider is usually sufficient. For example, in Node.js, the connection might look like this:
import { chromium } from 'playwright';
const endpoint = process.env.REMOTE_CDP_ENDPOINT;
if (!endpoint) throw new Error('REMOTE_CDP_ENDPOINT is required');
const browser = await chromium.connectOverCDP(endpoint, { timeout: 30_000 });
try {
const context = browser.contexts()[0];
if (!context) throw new Error('Default browser context not found');
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
console.log(await page.title());
} finally {
// This closes the Playwright connection but does not guarantee that the remote session stops.
await browser.close();
}
This is a general scheme, not a universal interface for all providers. Endpoint format, authentication, context creation rules, and behavior browser.close() depend on the service. Terminating the client connection does not always mean stopping the remote browser. To end the session, use the mechanism provided by the provider and check the confirmation of the release of resources.
How to connect Playwright to an Afina profile via a validated API
Afina runs the local HTTP API along with the desktop application and returns wsEndpoint after starting the profile. This example shows how to connect Playwright to a managed profile through the same CDP mechanism that some cloud services use. However, this is a local model, not a remote Browser as a Service: Chromium and profile data run on the user's infrastructure, and Playwright connects via CDP.
The base API address confirmed by the official product page looks like this http://127.0.0.1:50778. The key must be transmitted in the header x-api-key. Endpoint POST /api/profiles/start accepts profileId and returns wsEndpoint in the form ws://127.0.0.1:9223/devtools/browser/.... Before integration, read the full article about Afina API and automation via REST.
Don't expose your local API to the outside through simple port forwarding. If the Playwright client is running on a different server, use:
- controlled private channel
- access restrictions
- a separate model of threats
The local endpoint is intended for localhost, and CDP gives very broad rights over the running browser.
For remote access, use:
- secure transport such as TLS over a controlled gateway or private tunnel
- authentication
- network access rules
The CDP endpoint itself should not be considered a secure public API.
Before the main script, add:
- health check API
- checking the structure of the answer
If the application is not running or the port has changed, the error must be detected before the profile is reserved.
Also, pin a compatible Playwright version in the lockfile and re-check the required capabilities after updating Chromium. An automatic update without a regression test can break downloads, tracing or locators while the queue is already accepting production tasks.
Store your API key in a secret manager or secure local environment. Do not pass it as a command line argument: such values may be visible in the process list and CI logs.
For diagnosis, it is enough to write down:
- HTTP status
- endpoint name
- correlation ID
You do not need to write down the request headers.

The code below is a demo example, not a ready-to-run script. Function stopProfileUsingDocumentedApi() intentionally contains throw new Error() and will throw an error on shutdown. Before practical use, implement it according to the documentation of your specific version of Afina and check the actual stopping of the profile.
An unconfirmed endpoint for stopping is not listed here. The script consists of two parts: starting the profile via REST and connecting to the received endpoint. Get the profile ID and key from environment variables instead of writing them directly in your code.
import { chromium } from 'playwright';
const apiKey = process.env.AFINA_API_KEY;
const profileId = process.env.AFINA_PROFILE_ID;
if (!apiKey || !profileId) {
throw new Error('AFINA_API_KEY and AFINA_PROFILE_ID are required');
}
// Implement a regular stop according to the documentation for your Afina version.
// Do not use an unverified API path to stop the profile.
async function stopProfileUsingDocumentedApi(profileId) {
throw new Error('Configure a verified method for stopping the Afina profile');
}
let profileStarted = false;
let browser;
try {
const response = await fetch('http://127.0.0.1:50778/api/profiles/start', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': apiKey,
},
body: JSON.stringify({ profileId }),
});
if (!response.ok) {
throw new Error(`Failed to start the profile, HTTP ${response.status}`);
}
profileStarted = true;
const payload = await response.json();
if (typeof payload?.wsEndpoint !== 'string' || !payload.wsEndpoint) {
throw new Error('The API did not return a valid wsEndpoint');
}
browser = await chromium.connectOverCDP(payload.wsEndpoint, {
timeout: 30_000,
});
const context = browser.contexts()[0];
if (!context) throw new Error('Default browser context not found');
const page = context.pages()[0] ?? await context.newPage();
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
console.log(await page.title());
} finally {
// Closing the CDP connection does not guarantee that the profile stops.
try {
if (browser) await browser.close();
} finally {
if (profileStarted) await stopProfileUsingDocumentedApi(profileId);
}
}
The example above demonstrates the connection and script execution, but does not cover the profile's full lifecycle: the stop call remains a required function to implement according to the documentation for your Afina version. That function should also verify that resources were actually released. Do not treat browser.close() as a universal method for an orderly profile shutdown.
Depending on how the service is connected and implemented, closing the browser object, disconnecting the client, and terminating the Chromium process can have different consequences. Check the behavior of your version of Afina and when finished, call the documented profile stop method via the API.
If the API confirmed the launch, but the response or CDP connection was incorrect, the external finally also calls the stop procedure. An indeterminate run result requires an additional profile state check via the documented API.
For parallel execution, assign a separate profile to each active task and limit the number of simultaneous executions (concurrency) with a semaphore. The practical sequence is as follows:
- get a lease on a free profile
- run the profile through the local API
- connect Playwright to the CDP endpoint
- check the support of the required capabilities (capability probe) and execute the main script
- record the checkpoint and status of the business operation
- disconnect the client and stop the profile normally
- release the lease only after confirming the stop
If the task falls between steps five and six, the reconciler must find a profile without an active owner and complete it. This prevents the accumulation of unfinished local processes and the erroneous reuse of a busy session.
When Afina is appropriate
Afina is appropriate in this architecture when isolated profiles, a local REST API, and a direct CDP connection to Playwright are required. The infrastructure does not exempt the target platform from compliance, automation limitations, or the need for error handling. The material is provided for informational and educational purposes only.
Start with one test profile and one allowed scenario. Check the status after reconnect, browser restart and transport error, then increase the number of concurrent tasks targeting p95. Thus, the local profile becomes a manageable unit of execution, rather than a process whose completion must be monitored manually.
DownloadFAQ — Frequently Asked Questions
What is Playwright in the Cloud?
Playwright in the cloud manages a browser running on a remote or managed infrastructure. The client script connects over the network and executes the usual automation commands.
How do I connect Playwright to a browser via CDP?
Pass the HTTP or WebSocket endpoint to chromium.connectOverCDP(). Then get the default context via browser.contexts()[0].
How does connectOverCDP differ from browserType connect?
connectOverCDP() works with Chromium already running via CDP. browserType.connect() connects to Playwright BrowserServer and provides more precise integration.
Does CDP keep cookies after disconnection?
CDP itself does not store cookies. After the client is disconnected, the state remains available if the browser continues to run. After a restart, cookies can be restored from a persistent profile or a previously saved state, if they are still valid.
How to run Playwright jobs in parallel?
Assign a separate profile to each active task and limit concurrency to a queue or semaphore. Increase the limit after checking p95 and success rate.
Why does remote Playwright run slower?
Multiple consecutive commands can increase latency due to network traffic. Reduce the number of calls, choose a closer region and measure the stages separately.
Can Playwright be connected to an Afina profile?
Yes, starting the profile through the local API returns wsEndpoint for chromium.connectOverCDP(). The connection works with the default context of the running profile.
How to avoid duplicate actions after a WebSocket break?
Save a checkpoint after each data change operation. Before repeating, check the business result and use the idempotency key if the system supports it.
