We’ve upgraded Browser Run by rebuilding it on top of Cloudflare’s Containers, allowing for higher usage limits, faster performance, and better reliability.
With the Workers binding, you can now launch 60 browsers per minute and run up to 120 at the same time. This is 4 times the previous limit. Plus, Quick Action is now over 50% faster. You don’t need to do anything; these improvements are effective immediately. We are also releasing new features and bug fixes at a quicker pace. Keep reading to understand how we achieved this and to look at the performance data.
Quick recap: what is Browser Run?
Browser Run allows developers to control headless browsers running on Cloudflare’s global network through code. This is helpful for testing web apps, safely checking suspicious links, and quickly creating PDFs. It also supports tasks like taking screenshots and scraping content. More recently, it has become a vital tool for AI agents that need to browse the web. We are developing Browser Run as the primary platform for running automated browsers securely on a large scale.
Before we switched to Cloudflare Containers, we shared our systems with Browser Isolation (BISO). Even though the technologies were similar, BISO’s larger container images caused slower startup times and reduced development speed. Importantly, BISO browsers were not optimally distributed across the globe, which hurt both speed and reliability. Also, the long, steady sessions typical for BISO users didn’t align with the short, sudden spikes in usage that Browser Run requires, leading to delays and scaling issues.
Luckily, after significant internal work, Cloudflare launched its open beta for Durable Object (DO)-enabled Containers last year. This allowed us to start the adoption process, which turned out to benefit both platforms. Like many successful tech teams, we prefer to build on our own platform first so we can identify and fix issues before our external users run into them.
The migration to Containers
We started the move slowly by adding a Worker to handle incoming requests. This let us offer Containers-based browsers to a few select users while others stayed on BISO. Supporting both during the transition was essential for comparing performance, finding bugs, and ensuring the new system was an improvement.
As we expanded, we first moved our Quick Actions endpoints to Containers, then moved free accounts using the Workers browser binding, followed by pay-as-you-go accounts. This step-by-step testing allowed us to confirm stability before moving all remaining business customers, a switch that required no updates or redeployments from our users.
Challenges: performance and scaling issues
However, on our side, we had to deal with several new hurdles while getting used to the Containers platform. It was an early-stage system with limited documentation, poor internal metrics, and few team members available in overlapping time zones. Acting as Customer Zero meant we could provide direct feedback to our own teams, leading to major improvements that helped our outside users. Still, there was plenty of initial difficulty, which is normal for a product in active development. Other issues were specific to the new technical setup.
For instance, once our browsers were truly available worldwide, we had to change how our system worked. DO-enabled Containers create a Durable Object near the incoming request, but the actual Container might start up on the other side of the planet. This works for simple commands like “start my app,” but when you are building a WebSocket and sending many messages for a screenshot, those extra milliseconds of global travel slow things down.
Our fix was to create regional groups of pre-warmed DO-backed browser containers to reduce the distance (and the delay) between DOs and containers. When a request arrives, we match it with a DO-container pair that is physically closest to the user. This cuts down on latency for both steps: from user to DO and from DO to Container. While this added more complexity to our setup, we decided it was a worthy trade-off as long as we could track the status of every browser to manage resources based on current needs. This was a great job for Workers KV…up to a certain extent.
The need for headless browsers has grown significantly since the start of last year. AI agent developers found Browser Run and quickly generated more traffic than we could initially manage. We quickly reached the limit of how fast we could adjust our pool sizes to handle this growth. Because of KV’s eventual consistency—which takes about 30 seconds—it became a sticking point in our main request flow. You might look at KV and see a container as “available,” but by the time you direct traffic to it (30 seconds later), someone else has already taken it. These slow updates caused race conditions and too many browsers being allocated, which made it very hard to keep up with sudden jumps in demand.
Migrating from KV to D1 and Queues
In the past, we saved each container’s status in KV. However, this approach often meant retrieving data that was up to a minute stale, due to cache TTL restrictions. While KV recently adjusted its minimum cache TTL to 30 seconds, this delay was still too long for our needs.
To solve this, we shifted our container state to D1 databases. D1’s support for transactions is ideal here. Once a browser is assigned to a specific user, no one else can use it. Because browsers are single-user resources, SQLite transactions provide the necessary atomicity to prevent multiple users from accidentally claiming the same browser at the same time.
Below is a basic example of the SQL query we use to acquire browsers:
WITH candidate_pool AS (
-- candidate pool logic to pick based on latency and other rules
)
UPDATE containers
SET status = 'picked'
WHERE sessionId IN (
SELECT sessionId
FROM candidate_pool
ORDER BY RANDOM()
LIMIT ?5
)
RETURNING dataWe utilize multiple D1 database instances in each geographic region. A challenge quickly arose: with potentially thousands of containers refreshing their state every 5 seconds, the database became overwhelmed. For example, with each write operation taking approximately 1ms, the maximum throughput was 1,000 operations per second, limiting us to just 5,000 containers.
The solution was to group these write operations. Grouped writes are much more efficient in terms of time, boosting throughput significantly. We now use batches of 100 rows, allowing us to handle up to 500,000 containers per region. This provides enough capacity to support growth without worrying about database limits.
Currently, our P95 for batch write performance is a mere 0.1ms!
To manage these batched operations, we use Cloudflare Queues. Every 5 seconds, each container generates its current status and pushes it into a regional queue. We then set up a queue consumer with a batch size of 100 and a batch timeout of 1 second:
{
...
"queues": {
"consumers": [
{
"queue": "production-core-containers-queue-weur",
"max_batch_size": 100,
"max_batch_timeout": 1,
"max_retries": 1,
},
...
]
...
}
}This system achieves a consistent queue delay (lag) of under two seconds. Should delays increase and the data become stale, each region can switch to a backup region until the primary queue returns to normal operation.
Bonus Advantages for Quick Actions
Having our own infrastructure also allowed us to upgrade browser container images without affecting other offerings like BISO. This freedom enabled us to optimize “quick actions,” such as taking screenshots and extracting page content. In the past, our servers used a WebSocket connection to the browser, requiring multiple individual steps: loading a page, navigating to a URL, waiting for it to load, and finally taking a screenshot. Each step had to wait for the previous one to finish.
Now, we send all parameters as a single HTTP request directly to the container, which executes the actions internally without needing to communicate back and forth with the main server.
Results: Significant Speed Improvement and Expanded Capacity
We have observed a dramatic reduction in response times for quick actions. Users can get their results much faster, both from quicker browser setups and more efficient DevTools Protocol message processing.
Successfully managing real-time data at this scale enabled us to dedicate time to experimentation, leading to the creation of new features like the recently released /crawl endpoint.
Enhanced Browser Customization
An added benefit of having our own containers is the ability to upgrade browser versions much faster.
Previously, using shared Browser Isolation containers meant updating Chrome required careful coordination among many teams, each with different goals and timelines. Now that we manage our own container environment, we can make updates more frequently. This allows us to offer requested features, such as WebGL for advanced browser rendering and WebMCP (Model Context Protocol for the web) for improved agent interactions. Both upgrades are possible because we can precisely manage browser versions and settings without disrupting other Cloudflare products.
In short, we are only beginning to scratch the surface of what’s possible with browsers at scale, especially for building AI agents. We encourage you to explore these capabilities — visit our documentation.
Browser Run is included in all Workers plans. Get started with our quick start guide, experiment with the Quick Actions, or use the /crawl endpoint to extract data from any website, following links to gather more content.
Building AI agents? Check out our Agents SDK, which offers built-in support for Browser Run.



