Mobile Proxy for Apify
Apify's SDK has first-class support for custom proxies via Actor.createProxyConfiguration with a proxyUrls list. Pass in mobile proxy URLs, hand the config to a CheerioCrawler or PlaywrightCrawler, and you're done.
Prerequisites
- →Apify CLI (
npm i -g apify-cli) and an Actor scaffold (apify create my-actor). - →Mobile proxy credentials from mobileproxies.org.
- →Apify SDK v3+ (
apify@^3,crawlee@^3).
Step-by-Step Configuration
Add credentials to Actor input schema
In INPUT_SCHEMA.json expose fields so they're encrypted on Apify and don't appear in logs:
{
"title": "Mobile Proxy Scraper",
"type": "object",
"schemaVersion": 1,
"properties": {
"proxyHost": { "type": "string", "title": "Proxy host", "default": "proxy.mobileproxies.org" },
"proxyPort": { "type": "integer", "title": "Proxy HTTP port", "default": 8000 },
"proxyUser": { "type": "string", "title": "Proxy username", "isSecret": true },
"proxyPass": { "type": "string", "title": "Proxy password", "isSecret": true },
"apiKey": { "type": "string", "title": "mobileproxies.org API key", "isSecret": true }
},
"required": ["proxyHost", "proxyPort", "proxyUser", "proxyPass"]
}Create the proxy configuration in main.js
import { Actor } from 'apify';
import { CheerioCrawler } from 'crawlee';
await Actor.init();
const input = await Actor.getInput();
const { proxyHost, proxyPort, proxyUser, proxyPass } = input;
const proxyUrl = `http://${proxyUser}:${proxyPass}@${proxyHost}:${proxyPort}`;
// Single mobile slot — give it to Apify as a "pool of one"
const proxyConfiguration = await Actor.createProxyConfiguration({
proxyUrls: [proxyUrl],
});Wire it into CheerioCrawler
const crawler = new CheerioCrawler({
proxyConfiguration,
maxRequestsPerCrawl: 500,
maxConcurrency: 5,
useSessionPool: true, // sticky sessions per request key
persistCookiesPerSession: true,
requestHandler: async ({ request, $, log, session }) => {
const title = $('title').text();
log.info(`[${session.id}] ${request.url} → ${title}`);
await Actor.pushData({ url: request.url, title });
},
failedRequestHandler: async ({ request, log }) => {
log.warning(`Giving up on ${request.url} after retries`);
},
});
await crawler.run(['https://example.com', 'https://example.org']);
await Actor.exit();PlaywrightCrawler variant
Identical wiring — just swap the crawler class:
import { PlaywrightCrawler } from 'crawlee';
const crawler = new PlaywrightCrawler({
proxyConfiguration,
launchContext: { launchOptions: { headless: true } },
requestHandler: async ({ page, request }) => {
await page.waitForLoadState('networkidle');
const html = await page.content();
await Actor.pushData({ url: request.url, htmlLength: html.length });
},
});Rotate the egress IP from inside the Actor
async function rotate() {
const r = await fetch(
'https://buy.mobileproxies.org/api/v1/proxies/us-mob-01/switch',
{ method: 'POST', headers: { Authorization: `Bearer ${input.apiKey}` } },
);
if (!r.ok) throw new Error('rotate failed: ' + r.status);
await new Promise(r => setTimeout(r, 4000)); // wait for re-bind
}
// Call rotate() between batches, or inside failedRequestHandler.Verify It Works
Add a one-off request handler that fetches the egress IP and logs the ASN:
requestHandler: async ({ session, log }) => {
const proxyUrl = await proxyConfiguration.newUrl(session.id);
const r = await fetch('https://api.ipify.org?format=json', {
// crawlee handles the agent automatically; in raw fetch use undici ProxyAgent
});
log.info('egress IP: ' + (await r.text()));
}Session Pool Patterns
Apify's SessionPool manages per-session cookies on top of the proxy. The mobile proxy is sticky (one egress IP per slot at a time), so the session pool maps to logical sessions: each session keeps its own cookie jar, headers and retry counter. When a session burns (4xx/5xx pattern), Apify retires it and creates a fresh one without you touching the proxy URL.
For truly different egress IPs, schedule rotations through our API or run multiple Apify Actors against multiple slots — each slot is its own egress identity.
Common Errors
"All proxy URLs failed, retiring session"
Crawlee marks the proxy bad after 3 consecutive non-2xx responses. Lower maxConcurrency and add a preNavigationHooks jitter — bursting on a single mobile IP looks bot-like.
"407 Proxy Authentication Required"
The proxy URL was passed without URL-encoding special characters in the password. Use encodeURIComponent(pass) when constructing the string.
Playwright leaks the real IP via WebRTC
Pass --disable-features=WebRtcHideLocalIpsWithMdns to launchOptions.args, or use the playwright-extra stealth plugin.
Related Guides
Drop Mobile IPs Into Your Apify Actor
$5 trial. proxyUrls just works. Real carrier ASNs, rotation API, sticky sessions.