Skip to content
JavaScript SDK

Proxy & events

Platform proxy, Scrape.ly browser location, platform events, state, and reboots.

This page covers how an Actor routes traffic through proxies, reacts to platform events, keeps state across restarts, and reports progress. For proxy pricing and the dashboard Proxy page, see Proxy.

Proxy configuration

Actor.createProxyConfiguration(options?) returns a ProxyConfiguration for crawlers and fetch clients, or undefined when there's no proxy to use. It works in three modes:

ModeWhenTraffic goes through
Platform proxyNo proxyUrls, newUrlFunction, tieredProxyUrls, or tieredProxyConfig given, and the run has platform proxy credentialsScrape.ly's proxy, residential by default
Your own proxiesAny of those options is givenThe URLs you supply
MixedYour URL lists contain 'auto' (or 'http://auto')Your URLs, with 'auto' slots served by the platform proxy

Platform proxy

Platform runs get proxy credentials in SCRAPELY_PROXY_HOSTNAME, SCRAPELY_PROXY_PORT, SCRAPELY_PROXY_USERNAME, SCRAPELY_PROXY_PASSWORD, and SCRAPELY_PROXY_STATUS_URL (mirrored as APIFY_PROXY_*). The password is the run ID, and the proxy only accepts connections from that run's container.

.ts
import { Actor, CheerioCrawler } from 'scrapely';

await Actor.main(async () => {
    const proxyConfiguration = await Actor.createProxyConfiguration({
        groups: ['RESIDENTIAL'],
        countryCode: 'US',
    });

    const crawler = new CheerioCrawler({
        proxyConfiguration,
        async requestHandler({ request, $, proxyInfo }) {
            await Actor.pushData({ url: request.url, title: $('title').text(), country: proxyInfo?.countryCode });
        },
    });
    await crawler.run(['https://example.com']);
});
OptionMeaning
groups['RESIDENTIAL'] (default pool) or ['DATACENTER']. Other groups, such as GOOGLE_SERP, are refused
countryCodeTwo-letter ISO country code, e.g. 'US'
subdivisionCodeISO 3166-2 subdivision (1–3 characters), requires countryCode. The proxy currently routes by country only
passwordOverrides the injected proxy password
scrapelyProxyGroups, scrapelyProxyCountry, scrapelyProxySubdivisionThe same three options under their input-field names. The Apify aliases apifyProxyGroups, apifyProxyCountry and apifyProxySubdivision also work; the Scrapely name wins when both are set (Apify names)

The SDK encodes these options in the proxy username, e.g. groups-RESIDENTIAL,session-abc,country-US. Crawlers pass a session ID to keep the same exit IP per session. Session IDs are at most 50 characters from a-z, A-Z, 0-9, ., _, and ~; anything else throws. ProxyInfo from the platform proxy also carries groups, countryCode, and subdivisionCode.

Proxy traffic is billed per GB to the run and counts toward the run's maxTotalChargeUsd.

Your own proxies

.ts
const proxyConfiguration = await Actor.createProxyConfiguration({
    proxyUrls: ['http://user:pass@proxy-a.example.com:8000', 'http://user:pass@proxy-b.example.com:8000'],
});

You can also pass newUrlFunction(sessionId), or tiers:

.ts
const proxyConfiguration = await Actor.createProxyConfiguration({
    tieredProxyUrls: [
        [null],                                    // tier 0: no proxy
        ['auto'],                                  // tier 1: platform proxy
        ['http://user:pass@premium.example.com:8000'],
    ],
});

tieredProxyConfig takes a list of option objects instead. A tier with proxyUrls uses them; a tier without proxyUrls becomes a platform proxy tier with its own groups and countryCode. Tiers with newUrlFunction aren't supported and throw.

Errors and undefined

createProxyConfiguration() returns undefined when:

  • useScrapelyProxy: false is set and no proxy URLs, function, or tiers are given. With proxyUrls, useScrapelyProxy: false still uses your proxies.
  • No proxy URLs, function, or tiers are given and the run has no platform proxy credentials (for example, a local run). The SDK logs a warning explaining how to set SCRAPELY_PROXY_* locally from the credentials on the dashboard Proxy page. Using the platform proxy from outside Scrape.ly requires an active plan or a prepaid balance of at least $1.

It throws when:

  • you combine proxyUrls or newUrlFunction with groups, countryCode, or subdivisionCode (Cannot combine custom proxies with Scrapely Proxy!);
  • you pass both proxyUrls and newUrlFunction;
  • you use 'auto' or a URL-less tier without platform proxy credentials;
  • countryCode isn't two letters, or subdivisionCode is invalid or has no countryCode;
  • on the platform, the proxy refuses the configuration, for example a group other than RESIDENTIAL or DATACENTER (Proxy group "GOOGLE_SERP" is not available on Scrapely. Available groups: RESIDENTIAL, DATACENTER.).

checkAccess (default true) sends a test request through the platform proxy (two attempts) and logs the result. When the proxy refuses the configuration, createProxyConfiguration() throws on the platform and logs a warning in local runs. When the check gets no answer or an HTTP error, it only logs a warning. Pass checkAccess: false to skip it; the proxy still refuses requests for unknown groups with 407.

Remote browser location

When the Actor's Use Scrapely browser setting is on, browser crawlers connect to the Scrape.ly browser over CDP. To pick its exit location and fingerprint, create a BrowserConfiguration and pass it next to the crawler's other options:

.ts
import { Actor, PlaywrightCrawler } from 'scrapely';

await Actor.main(async () => {
    const browserConfiguration = await Actor.createBrowserConfiguration({
        countryCode: 'US',
        state: 'California',
        os: 'windows',
    });

    const crawler = new PlaywrightCrawler({
        browserConfiguration,
        async requestHandler({ request, page }) {
            await Actor.pushData({ url: request.url, title: await page.title() });
        },
    });
    await crawler.run(['https://example.com']);
});
OptionMeaning
countryCodeExit country, two-letter ISO code
stateRegion name, e.g. 'California' (not an ISO code)
cityCity name, e.g. 'San Jose'
osFingerprint OS: 'windows', 'macos', or 'android'
timezoneIANA timezone, up to 64 characters. Defaults to the exit country's main timezone
languageComma-separated language tags without q-values, e.g. 'de-CH,de' (up to 128 characters)
adblockAd and tracker blocking; off by default
solveCloudflareAutomatic Cloudflare Turnstile solving; on by default
browserCountry, browserState, browserCityInput-schema aliases for countryCode, state, city

Invalid values throw BrowserConfiguration: "<field>" .... Locally, and in builds with the setting off, the option is accepted and ignored.

The browser's location comes from the first of these that is set:

  1. browserConfiguration;
  2. the country (and subdivision, as a state) of a platform proxyConfiguration on the same crawler;
  3. the run's default country.

Proxy groups and session IDs don't apply to the Scrape.ly browser.

Events and graceful shutdown

.ts
Actor.on('persistState', saveCheckpoint);
Actor.off('persistState', saveCheckpoint);

Register listeners after Actor.init() (inside Actor.main()), so they attach to the platform event manager. off() without a listener removes all listeners for that event.

EventWhenPayload
persistStateEvery 60 seconds (persistStateIntervalMillis), and before a reboot, abort, or cost-limit stop{ isMigrating: boolean }
systemInfoPeriodically on platform runscpuCurrentUsage, cpuAvgUsage, cpuMaxUsage, memCurrentBytes, memAvgBytes, memMaxBytes, isCpuOverloaded, createdAt
cpuInfoPeriodically on platform runs{ isCpuOverloaded: boolean }
migratingA reboot was requested; the container stops a few seconds later—
abortingThe run is being aborted: a graceful abort, an exhausted balance, or the run's cost limit—

systemInfo also feeds Crawlee's autoscaling. The SDK reconnects to the events WebSocket every 5 seconds if the connection drops.

With gracefulShutdown on (the default), the SDK handles the last two for you: aborting calls Actor.exit() and migrating calls Actor.reboot(), after gracefulShutdownDelayMillis. Don't call Actor.reboot() from your own migrating listener as well. Save state in a persistState listener, or use useState().

After aborting the platform stops the container within about 30 seconds, so keep cleanup short.

Actor.events returns the underlying EventEmitter. Before init() it's a standalone emitter; after init() it's the platform event manager's.

State persistence and migration

Actor.useState()

useState() returns an object that is saved to a key-value store on every persistState event:

.ts
const state = await Actor.useState('crawl-state', { processed: 0, cursor: null as string | null });
state.processed += 1;

Mutate the returned object in place; don't reassign it. It must be JSON-serializable. If the record already exists (for example after a reboot), you get the saved value instead of the default.

CallRecord key
Actor.useState('crawl-state', { ... })crawl-state
Actor.useState({ ... })STATE
Actor.useState()APIFY_GLOBAL_STATE (legacy default)

The third argument selects another store:

.ts
const state = await Actor.useState('crawl-state', { cursor: 0 }, { keyValueStoreName: 'state-store' });

Actor.reboot()

reboot() restarts the container while keeping the run, its ID, and its storages. It:

  1. emits persistState with isMigrating: true and waits 1 second;
  2. sends POST /v2/actor-runs/{runId}/reboot;
  3. waits briefly, flushes storage and charges, and calls process.exit(0).

Code after await Actor.reboot() doesn't run. The new container starts the Actor from the beginning, so restore progress from useState() or your own records. Only RUNNING, STANDBY, and READY runs can be rebooted.

Status messages

.ts
await Actor.setStatusMessage('Downloading page 3');
await Actor.setStatusMessage('Completed', { isStatusMessageTerminal: true });

setStatusMessage(message, options?) logs [Status message]: <message> at level (DEBUG, INFO, WARNING, or ERROR; default INFO) and updates the run with PUT /v2/actor-runs/{runId} (statusMessage, isStatusMessageTerminal). The level only affects the log line. It returns the updated run, or {} when there are no run credentials (local runs). It throws if the API rejects the update.

isStatusMessageTerminal: true marks the message as final for the run. Use it for the last message before exiting.