Run Concurrent Browser Sessions
Launch multiple browser sessions in parallel to speed up large-scale scraping, testing, or automation. Each session runs independently on Browserless infrastructure.
- A Browserless API token from your account dashboard
- A Browserless plan that supports the concurrency level you need
Steps
- REST API
- Frameworks
Each REST request opens an independent browser session on Browserless. Fire them concurrently using your language's async primitives. No WebSocket connection needed.
- cURL
- Node.js
- Python
- Java
- C#
- PHP
- Ruby
1. Send requests in parallel
Each REST API call opens an independent browser session. Run them concurrently with &:
curl -s -X POST \
"https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/page/1", "options": { "type": "png", "fullPage": true } }' \
--output page-1.png &
curl -s -X POST \
"https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/page/2", "options": { "type": "png", "fullPage": true } }' \
--output page-2.png &
curl -s -X POST \
"https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/page/3", "options": { "type": "png", "fullPage": true } }' \
--output page-3.png &
wait
echo "All screenshots saved"
2. Check the output
All three screenshots are captured simultaneously. Total time is roughly the duration of the slowest single request.
1. Fire concurrent requests with Promise.all
import fs from 'fs';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const URLS = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
];
await Promise.all(
URLS.map((url, i) =>
fetch(
`https://production-sfo.browserless.io/screenshot?token=${TOKEN}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, options: { type: 'png', fullPage: true } }),
}
).then(async (res) => {
const buf = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(`screenshot-${i + 1}.png`, buf);
console.log(`Saved screenshot-${i + 1}.png`);
})
)
);
console.log('All done');
2. Check the output
Run with node concurrent.mjs. All 5 screenshots are captured in parallel — total time is the slowest single page, not 5× sequential.
1. Install dependencies
pip install requests
2. Use ThreadPoolExecutor for parallel requests
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
TOKEN = 'YOUR_API_TOKEN_HERE'
URLS = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
]
def capture(i_url):
i, url = i_url
res = requests.post(
f'https://production-sfo.browserless.io/screenshot?token={TOKEN}',
json={'url': url, 'options': {'type': 'png', 'fullPage': True}},
)
filename = f'screenshot-{i + 1}.png'
with open(filename, 'wb') as f:
f.write(res.content)
print(f'Saved {filename}')
with ThreadPoolExecutor(max_workers=5) as executor:
futures = [executor.submit(capture, (i, url)) for i, url in enumerate(URLS)]
for f in as_completed(futures):
f.result()
print('All done')
3. Check the output
Run with python concurrent.py. All 5 screenshots are captured in parallel.
1. Dependencies
java.net.http.HttpClient ships with the JDK (Java 11+). No extra dependencies needed.
2. Fire concurrent requests with CompletableFuture
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;
public class ConcurrentScreenshots {
public static void main(String[] args) {
String token = "YOUR_API_TOKEN_HERE";
List<String> urls = List.of(
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
"https://example.com/page/4",
"https://example.com/page/5"
);
HttpClient client = HttpClient.newHttpClient();
CompletableFuture<?>[] futures = IntStream.range(0, urls.size())
.mapToObj(i -> {
String body = "{\"url\":\"" + urls.get(i) + "\",\"options\":{\"type\":\"png\",\"fullPage\":true}}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
"https://production-sfo.browserless.io/screenshot?token=" + token))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return client
.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray())
.thenAccept(res -> {
try {
Files.write(Path.of("screenshot-" + (i + 1) + ".png"), res.body());
System.out.println("Saved screenshot-" + (i + 1) + ".png");
} catch (Exception e) {
throw new RuntimeException(e);
}
});
})
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
System.out.println("All done");
}
}
3. Check the output
Compile with javac ConcurrentScreenshots.java and run with java ConcurrentScreenshots. allOf blocks until the slowest request finishes.
This example builds the JSON body via string concatenation. For production use, add a library like Jackson to serialize the body safely — URLs containing quotes or backslashes will break the raw string approach.
1. Dependencies
System.Net.Http.HttpClient is part of the .NET standard library. No packages needed.
2. Fire concurrent requests with Task.WhenAll
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class ConcurrentScreenshots
{
static async Task Main()
{
const string token = "YOUR_API_TOKEN_HERE";
string[] urls = {
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
"https://example.com/page/4",
"https://example.com/page/5",
};
using var client = new HttpClient();
await Task.WhenAll(urls.Select(async (url, i) =>
{
var body = new StringContent(
$"{{\"url\":\"{url}\",\"options\":{{\"type\":\"png\",\"fullPage\":true}}}}",
Encoding.UTF8,
"application/json"
);
var response = await client.PostAsync(
$"https://production-sfo.browserless.io/screenshot?token={token}",
body
);
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync($"screenshot-{i + 1}.png", bytes);
Console.WriteLine($"Saved screenshot-{i + 1}.png");
}));
Console.WriteLine("All done");
}
}
3. Check the output
Run with dotnet run. Task.WhenAll fires all requests concurrently and waits for the last one to finish.
This example builds the JSON body via string interpolation. For production use, use System.Text.Json.JsonSerializer.Serialize instead — URLs containing quotes or backslashes will break the raw string approach.
1. Install dependencies
composer require react/http
2. Send requests concurrently with ReactPHP
ReactPHP's event loop runs all requests in parallel without spawning threads:
<?php
require 'vendor/autoload.php';
use React\Http\Browser;
use React\EventLoop\Loop;
$token = 'YOUR_API_TOKEN_HERE';
$urls = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
];
$browser = new Browser();
$promises = [];
foreach ($urls as $i => $url) {
$index = $i + 1;
$promises[] = $browser
->post(
"https://production-sfo.browserless.io/screenshot?token={$token}",
['Content-Type' => 'application/json'],
json_encode(['url' => $url, 'options' => ['type' => 'png', 'fullPage' => true]])
)
->then(function ($response) use ($index) {
file_put_contents("screenshot-{$index}.png", (string) $response->getBody());
echo "Saved screenshot-{$index}.png\n";
});
}
\React\Promise\all($promises)->then(function () {
echo "All done\n";
});
Loop::run();
3. Check the output
Run with php concurrent.php. Loop::run() processes all requests through the event loop until the last promise resolves.
1. Dependencies
net/http and json are part of Ruby's standard library. No gems required.
2. Spawn one thread per URL
require 'net/http'
require 'json'
require 'uri'
TOKEN = 'YOUR_API_TOKEN_HERE'
URLS = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
].freeze
threads = URLS.each_with_index.map do |url, i|
Thread.new do
uri = URI("https://production-sfo.browserless.io/screenshot?token=#{TOKEN}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ url: url, options: { type: 'png', fullPage: true } })
response = http.request(request)
File.binwrite("screenshot-#{i + 1}.png", response.body)
puts "Saved screenshot-#{i + 1}.png"
end
end
threads.each(&:join)
puts 'All done'
3. Check the output
Run with ruby concurrent.rb. join waits for all threads before printing the final line.
Each connect() call opens a separate managed browser on Browserless. Use Promise.all or goroutines to run them in parallel. Sessions are completely isolated from each other.
- Puppeteer
- Playwright
- Go (chromedp)
1. Install dependencies
npm install puppeteer-core
2. Open multiple browser connections in parallel
Each puppeteer.connect() call opens a separate browser session on Browserless:
import puppeteer from 'puppeteer-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const WS = `wss://production-sfo.browserless.io?token=${TOKEN}`;
const URLS = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
];
const results = await Promise.all(
URLS.map(async (url, i) => {
const browser = await puppeteer.connect({ browserWSEndpoint: WS });
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const title = await page.title();
console.log(`[${i + 1}] ${title}`);
return { url, title };
} finally {
// Always close to release the session even on error.
await browser.close();
}
})
);
console.log('Results:', results);
3. Check the output
Run with node concurrent.mjs. Each session runs in its own browser process on Browserless — no shared state between them.
1. Install dependencies
npm install playwright-core
2. Open multiple connections in parallel
import { chromium } from 'playwright-core';
const TOKEN = 'YOUR_API_TOKEN_HERE';
const WS = `wss://production-sfo.browserless.io?token=${TOKEN}`;
const URLS = [
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4',
'https://example.com/page/5',
];
const results = await Promise.all(
URLS.map(async (url, i) => {
const browser = await chromium.connectOverCDP(WS);
try {
// Use the default context — browser.newPage() creates a new context that
// doesn't inherit proxy, profile, or launch settings.
const context = browser.contexts()[0];
const page = await context.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
const title = await page.title();
console.log(`[${i + 1}] ${title}`);
return { url, title };
} finally {
// Always close to release the session even on error.
await browser.close();
}
})
);
console.log('Results:', results);
3. Check the output
Run with node concurrent.mjs. Each session runs in its own browser process on Browserless.
1. Install dependencies
go get github.com/chromedp/chromedp
2. Spawn one goroutine per URL
Each goroutine gets its own NewRemoteAllocator context, which opens a separate browser session on Browserless:
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/chromedp/chromedp"
)
func main() {
token := "YOUR_API_TOKEN_HERE"
ws := fmt.Sprintf("wss://production-sfo.browserless.io?token=%s", token)
urls := []string{
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
"https://example.com/page/4",
"https://example.com/page/5",
}
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(i int, url string) {
defer wg.Done()
// Each goroutine gets its own allocator — a separate browser session per URL.
allocCtx, cancel := chromedp.NewRemoteAllocator(
context.Background(), ws, chromedp.NoModifyURL,
)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
var title string
if err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Title(&title),
); err != nil {
log.Printf("[%d] error: %v", i+1, err)
return
}
fmt.Printf("[%d] %s\n", i+1, title)
}(i, url)
}
wg.Wait()
fmt.Println("All done")
}
3. Check the output
Run with go run main.go. Each goroutine runs independently — output lines may arrive out of order since sessions finish at different times.
Tips
- Concurrency limits — your plan determines how many sessions can run simultaneously. Requests beyond that limit queue automatically.
- Reuse a saved profile — add
&profile=my-profileto the WebSocket URL to start every concurrent session pre-authenticated. See Save Logins to Authenticated Profiles. - Rate limiting — add a concurrency cap in your own code (e.g.
p-limitin Node.js) to avoid overwhelming the target site.
Next steps
- Save Logins to Authenticated Profiles — share a login state across all concurrent sessions
- Scrape Structured Data — extract data at scale from many pages in parallel
- Take a Screenshot — capture screenshots in bulk