Integrating Puppeteer with AdsPower for 50-profile automation
Integrating Puppeteer with AdsPower for 50-profile automation
Running browser automation at scale means dealing with fingerprinting, account bans, and the overhead of managing dozens of independent browser environments. pure headless Chrome gets flagged fast on any serious platform. AdsPower solves the fingerprint problem by giving each profile its own isolated browser context with unique canvas, WebGL, audio, and font fingerprints. the missing piece for most operators is hooking their automation code into those profiles without clicking through a GUI one by one.
AdsPower exposes a local REST API that lets you start any profile programmatically and receive a WebSocket endpoint you can hand directly to Puppeteer. that means you can script your entire workflow in Node.js, loop over 50 profiles with concurrency controls, and treat the antidetect browser as infrastructure rather than a manual tool. i’ve been running this setup for airdrop farming and account warming on several projects out of Singapore, and it’s the most reliable way i’ve found to keep automation maintainable as the profile count grows.
this tutorial walks through the full setup: prerequisites, the AdsPower local API, a reusable Puppeteer connector, a concurrent runner for 50 profiles, and what breaks when you try to push past that. by the end you’ll have a working script you can extend for your specific task.
what you need
- AdsPower desktop app installed on Windows or macOS (Linux is not officially supported as of May 2026). download from adspower.com.
- AdsPower paid plan that covers at least 50 profiles. the free tier caps at 2. check their current pricing page for exact tier costs, as plans change quarterly.
- 50 browser profiles already created in AdsPower, each with a proxy assigned. profiles without proxies will share your host IP, which defeats the purpose.
- Node.js 18+ installed. grab it from nodejs.org.
- npm packages:
puppeteer-core,axios,p-limit. install withnpm install puppeteer-core axios p-limit. - A proxy pool. residential or mobile proxies per profile. if you’re sourcing these, proxyscraping.org/blog/ has comparisons of residential proxy providers worth reading before you commit to a pool.
- Basic JavaScript familiarity. you don’t need to be a Node expert, but you should be comfortable with async/await and reading error stacks.
estimated monthly cost at 50 profiles: AdsPower mid-tier plan plus a proxy subscription for 50 unique IPs. costs vary significantly depending on proxy type and provider. residential proxies from quality providers typically run $3-8 per GB depending on volume.
step by step
step 1: enable the AdsPower local API
open AdsPower, go to Settings > Local API, and toggle the service on. the default port is 50325. leave it at default unless you have a port conflict.
test it’s running by hitting this URL in your browser:
http://local.adspower.com:50325/api/v1/browser/local-active
expected output: {"code":0,"data":{"status":"Active"},"msg":"success"}. if you get a connection refused, AdsPower is not running or the API service wasn’t saved. restart the app and toggle the setting again.
if it breaks: some machines resolve local.[adspower](https://www.adspower.com/).com to 127.0.0.1 fine, others don’t. if DNS fails, replace the hostname with 127.0.0.1 in all your API calls.
step 2: fetch your profile IDs
AdsPower assigns each profile a user_id. you need these IDs to start profiles via the API. rather than copying them manually, pull the list programmatically.
const axios = require('axios');
const API_BASE = 'http://local.adspower.com:50325/api/v1';
async function getProfileIds(pageSize = 100) {
const res = await axios.get(`${API_BASE}/user/list`, {
params: { page: 1, page_size: pageSize }
});
if (res.data.code !== 0) throw new Error(`API error: ${res.data.msg}`);
return res.data.data.list.map(p => p.user_id);
}
getProfileIds().then(ids => console.log(ids));
run this with node getIds.js. you should see an array of 50 user ID strings. save this list or integrate the fetch into your main runner.
if it breaks: if list is empty, you’re probably querying the wrong group. add a group_id param or check the AdsPower local API docs at localapi-doc-en.adspower.com for filtering options.
step 3: write the profile start/stop helpers
async function startProfile(userId) {
const res = await axios.get(`${API_BASE}/browser/start`, {
params: { user_id: userId, open_tabs: 1 }
});
if (res.data.code !== 0) {
throw new Error(`Could not start profile ${userId}: ${res.data.msg}`);
}
return res.data.data.ws.puppeteer;
}
async function stopProfile(userId) {
await axios.get(`${API_BASE}/browser/stop`, {
params: { user_id: userId }
});
}
startProfile returns the WebSocket debugger URL that Puppeteer needs. stopProfile closes the browser and frees up system memory. always call stop when a task finishes, otherwise you’ll run out of RAM fast when looping over 50 profiles.
if it breaks: if ws.[puppeteer](https://pptr.dev/) is undefined, AdsPower started the browser but the debugging port wasn’t exposed. in AdsPower settings, make sure “Allow debug port” is enabled. some older versions of AdsPower had this off by default.
step 4: connect Puppeteer to a running profile
const puppeteer = require('puppeteer-core');
async function connectToProfile(wsEndpoint) {
const browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
defaultViewport: null
});
return browser;
}
defaultViewport: null is important here. if you pass a viewport, Puppeteer overrides AdsPower’s fingerprint-consistent viewport, which can cause dimension mismatches that detection systems pick up. the Chrome DevTools Protocol is what powers this connection under the hood. Puppeteer is just a high-level wrapper over it.
if it breaks: if Puppeteer throws Error: connect ECONNREFUSED, the browser process may have opened but the debug port wasn’t ready yet. add a small retry loop with a 1-second delay before connecting.
step 5: write a single-profile task function
wrap your actual automation logic in a function that takes a userId, handles the full lifecycle, and always cleans up.
async function runTask(userId) {
let browser = null;
try {
const wsEndpoint = await startProfile(userId);
browser = await connectToProfile(wsEndpoint);
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// your automation logic here
const title = await page.title();
console.log(`[${userId}] page title: ${title}`);
await page.close();
} catch (err) {
console.error(`[${userId}] task failed: ${err.message}`);
} finally {
if (browser) await browser.disconnect();
await stopProfile(userId);
}
}
the finally block ensures stopProfile runs even if the task throws. this is critical when running 50 profiles, one leaked process can cascade into a RAM spiral.
if it breaks: networkidle2 can time out on heavy sites. switch to { waitUntil: 'domcontentloaded' } or a specific element selector as your ready signal.
step 6: run 50 profiles with concurrency control
running all 50 profiles simultaneously will overwhelm most machines. 5-10 concurrent profiles is a reasonable starting point on a 16GB RAM machine. use p-limit to cap concurrency.
const pLimit = require('p-limit');
async function runAll(profileIds, concurrency = 5) {
const limit = pLimit(concurrency);
const tasks = profileIds.map(id => limit(() => runTask(id)));
const results = await Promise.allSettled(tasks);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.warn(`${failed.length} profiles failed`);
}
}
(async () => {
const ids = await getProfileIds();
await runAll(ids, 5);
console.log('all done');
})();
Promise.allSettled instead of Promise.all means one failing profile doesn’t abort the rest. check the failed array after the run to see which profiles need manual inspection.
if it breaks: if you hit EMFILE: too many open files, increase your system’s file descriptor limit. on Linux this is ulimit -n 4096. on macOS, set it via launchctl limit maxfiles.
step 7: add retry logic for flaky profiles
some profiles will fail to start due to proxy timeouts or AdsPower API hiccups. a simple retry wrapper prevents you from having to re-run the entire batch.
async function runTaskWithRetry(userId, retries = 2) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
await runTask(userId);
return;
} catch (err) {
if (attempt < retries) {
console.warn(`[${userId}] retry ${attempt + 1}: ${err.message}`);
await new Promise(r => setTimeout(r, 3000));
} else {
console.error(`[${userId}] gave up after ${retries + 1} attempts`);
}
}
}
}
replace runTask with runTaskWithRetry in your runAll call. for production, log failures to a file so you can re-queue them later without re-running successful profiles.
common pitfalls
not closing browsers between runs. AdsPower will show profiles as “running” even after your script crashes. open AdsPower’s profile list and force-close any stuck browsers before re-running, or your API calls will return an error about the profile already being active.
using [puppeteer](https://pptr.dev/) instead of [puppeteer](https://pptr.dev/)-core. [puppeteer](https://pptr.dev/) ships its own Chromium and will try to launch it instead of connecting to AdsPower’s instance. always use [puppeteer](https://pptr.dev/)-core and always connect via browserWSEndpoint.
overriding fingerprint properties in your script. setting page.setUserAgent(), page.setExtraHTTPHeaders(), or viewport overrides after connecting will undo what AdsPower set up. let AdsPower handle all the fingerprint properties. your script should only handle navigation and DOM interaction.
running all 50 profiles from one residential IP. if your host machine’s IP is visible in any way, platforms may flag coordinated traffic originating from the same source. each profile needs its proxy configured in AdsPower, not applied at the OS network level.
ignoring rate limits on the AdsPower local API. calling browser/start for 50 profiles in a tight loop with no delay can trip internal rate limiting. add a short delay between start calls, or batch them in the concurrency-limited queue as shown above, which naturally staggers the starts.
scaling this
10 to 100 profiles. the main bottleneck shifts from network to RAM. at 100 concurrent-capable profiles, you want a dedicated machine or VPS with 32GB+ RAM. keep concurrency at 8-10 active profiles simultaneously and let the queue drain. the code above handles this without changes.
100 to 1000 profiles. a single AdsPower instance on one machine won’t hold 1000 profiles comfortably. at this scale, operators typically shard across multiple machines, each running an AdsPower instance with a subset of profiles. you’d need a coordinator service to distribute profile IDs to each machine’s runner. this is where multi-account ops becomes its own engineering problem, and multiaccountops.com/blog/ has writeups on distributed account management architectures worth reading before you build your own.
1000+ profiles. beyond 1000, the per-profile fingerprint storage and proxy assignment overhead is significant. most operators at this scale move to a headless farm with a custom fingerprint injection layer, and use AdsPower primarily for account warming rather than bulk automation. the AdsPower local API approach described here can still be one node in a larger system, but it stops being the whole system.
where to go next
- AdsPower full review and plan comparison covers which plan tiers make sense for different operation sizes, and how AdsPower compares to Multilogin and Dolphin Anty on fingerprint quality.
- Proxy setup guide for antidetect browsers walks through assigning residential and mobile proxies to AdsPower profiles in bulk using the API, including how to rotate proxies between sessions.
- Back to the blog index for all tutorials on antidetect browsers, automation, and account management.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-05-19.