Environment & proxy
Run environment, Scrape.ly browser, platform proxy, and HTTP serving.
The platform describes each run to the container through environment variables, and the SDK parses them into a configuration object. This page lists what Actor.get_env() returns and which variables the platform sets. It also covers the remote browser, the Scrape.ly proxy and serving HTTP from a Python Actor.
Run environment
Actor.get_env()
Actor.get_env() is synchronous and works before init(). It returns an ApifyEnv object. You can read fields as attributes (env.actor_run_id) or by key (env["actor_run_id"]), and env.keys() lists the fields that are set. Fields without a value are None, except the storage IDs, which default to "default", and input_key, which defaults to "INPUT".
| Field | Source |
|---|---|
actor_id | ACTOR_ID |
actor_run_id | ACTOR_RUN_ID |
actor_task_id | ACTOR_TASK_ID |
actor_full_name | ACTOR_FULL_NAME |
actor_build_id, actor_build_number, actor_build_tags | ACTOR_BUILD_ID, ACTOR_BUILD_NUMBER, ACTOR_BUILD_TAGS |
default_dataset_id, default_key_value_store_id, default_request_queue_id | ACTOR_DEFAULT_*_ID |
actor_storages_json | ACTOR_STORAGES_JSON |
input_key | ACTOR_INPUT_KEY |
token | SCRAPELY_TOKEN, or APIFY_TOKEN |
api_base_url | SCRAPELY_API_URL, or APIFY_API_BASE_URL |
api_public_base_url | APIFY_API_PUBLIC_BASE_URL |
is_at_home | "1" when SCRAPELY_IS_AT_HOME or APIFY_IS_AT_HOME is truthy |
actor_events_ws_url | ACTOR_EVENTS_WEBSOCKET_URL |
container_port, container_url | ACTOR_WEB_SERVER_PORT, ACTOR_WEB_SERVER_URL |
memory_mbytes | ACTOR_MEMORY_MBYTES, as int |
started_at, timeout_at | ACTOR_STARTED_AT, ACTOR_TIMEOUT_AT, as datetime |
max_total_charge_usd | ACTOR_MAX_TOTAL_CHARGE_USD, as float |
actor_max_paid_dataset_items | ACTOR_MAX_PAID_DATASET_ITEMS, as int |
actor_pricing_info, charged_event_counts | APIFY_ACTOR_PRICING_INFO, APIFY_CHARGED_ACTOR_EVENT_COUNTS (JSON strings) |
restart_on_error | ACTOR_RESTART_ON_ERROR |
meta_origin | ACTOR_META_ORIGIN: how the run started (API, WEB, SCHEDULER, ACTOR, STANDBY, ...) |
standby_port, standby_url | ACTOR_STANDBY_PORT (4321), ACTOR_STANDBY_URL |
user_id, user_is_paying | APIFY_USER_ID, APIFY_USER_IS_PAYING |
headless | "1" when APIFY_HEADLESS is truthy |
proxy_hostname, proxy_port, proxy_password, proxy_status_url | APIFY_PROXY_* |
persist_state_interval_millis | APIFY_PERSIST_STATE_INTERVAL_MILLIS |
ApifyEnv also has compatibility fields, such as log_level, log_format, chrome_executable_path, dedicated_cpus and xvfb. Scrape.ly doesn't set them.
When a value is set under several names, the SDK applies them in this order, and later names win: APIFY_*, then SCRAPELY_*, then ACTOR_*, then CRAWLEE_STORAGE_DIR. So SCRAPELY_TOKEN overrides APIFY_TOKEN, and ACTOR_ID overrides APIFY_ACTOR_ID.
Variables the platform sets
You don't set these yourself. Most of them also have an APIFY_* copy.
| Variable | Purpose |
|---|---|
SCRAPELY_IS_AT_HOME | 1 on the platform |
SCRAPELY_API_URL | Internal API URL for this container |
SCRAPELY_TOKEN | Run-scoped API token |
ACTOR_ID, ACTOR_RUN_ID, ACTOR_TASK_ID, ACTOR_FULL_NAME | Run identity (also as APIFY_ACTOR_ID, APIFY_ACTOR_RUN_ID and the older APIFY_ACT_ID, APIFY_ACT_RUN_ID) |
ACTOR_BUILD_ID, ACTOR_BUILD_NUMBER, ACTOR_BUILD_TAGS | Build being run (also as APIFY_ACTOR_BUILD_ID, APIFY_ACTOR_BUILD_NUMBER) |
ACTOR_META_ORIGIN, APIFY_META_ORIGIN | How the run started: API, WEB, SCHEDULER, ACTOR, STANDBY, ... |
ACTOR_DEFAULT_DATASET_ID, ACTOR_DEFAULT_KEY_VALUE_STORE_ID, ACTOR_DEFAULT_REQUEST_QUEUE_ID | Default storages |
ACTOR_STORAGES_JSON | Storage aliases for this run |
ACTOR_INPUT, ACTOR_INPUT_KEY | Base64 JSON input (secrets already decrypted) and its record key |
ACTOR_METAMORPH_COUNT | Number of metamorphs so far |
ACTOR_EVENTS_WEBSOCKET_URL | Platform events |
CDP_URL | Scrape.ly browser WebSocket. Set on every run; used for launches only when the Actor's browser setting is on |
ACTOR_WEB_SERVER_PORT, ACTOR_WEB_SERVER_URL | HTTP port (4321) and the URL that proxies to it |
ACTOR_STANDBY_PORT | Port a standby server listens on (4321, the same port) |
ACTOR_MEMORY_MBYTES | Memory limit |
ACTOR_STARTED_AT, ACTOR_TIMEOUT_AT | ISO timestamps |
ACTOR_PERMISSION_LEVEL | LIMITED_PERMISSIONS (default) or FULL_PERMISSIONS |
ACTOR_RESTART_ON_ERROR | 1 or 0 |
ACTOR_MAX_TOTAL_CHARGE_USD, ACTOR_MAX_PAID_DATASET_ITEMS | Spending limits, when set |
APIFY_ACTOR_PRICING_INFO, APIFY_CHARGED_ACTOR_EVENT_COUNTS | Pay-per-event pricing and counts so far |
APIFY_USER_ID, APIFY_USER_IS_PAYING, SCRAPELY_USER_PLAN | Account details (SCRAPELY_USER_PLAN is paid or free) |
SCRAPELY_PROXY_HOSTNAME, SCRAPELY_PROXY_PORT, SCRAPELY_PROXY_USERNAME, SCRAPELY_PROXY_PASSWORD, SCRAPELY_PROXY_STATUS_URL | Platform proxy credentials, when proxy access is enabled |
SCRAPELY_LLM_BASE_URL | LLM gateway base URL (OpenRouter-compatible, ends in /v2/llm/api/v1). Use SCRAPELY_TOKEN as the API key |
ACTOR_STANDBY_URL, ACTOR_STANDBY_IDLE_TIMEOUT_SECS | Set for standby runs |
Environment variables defined on the Actor version are added too. They can't override the run identity variables above (ids, build ids, ACTOR_META_ORIGIN, ACTOR_STANDBY_PORT).
Configuration
Actor.configuration (or Actor.config, as in Apify's SDK) returns the parsed Configuration. Use get(key, default=None) and set(key, value) with the snake_case keys from the table above, or read them as attributes, e.g. Actor.config.standby_port. Off the platform, set SCRAPELY_API_URL and SCRAPELY_TOKEN yourself if you use force_cloud or the API clients.
Run helpers
| Member | Returns |
|---|---|
Actor.is_at_home() | True on the platform |
Actor.is_standby() | True when ACTOR_META_ORIGIN is STANDBY |
Actor.is_paying | True when the account has a paid plan |
Actor.get_remaining_time() | Milliseconds until ACTOR_TIMEOUT_AT, as int, or None without a timeout |
Actor.log | The SDK logger (logging.Logger) |
Remote browser
Where a browser runs depends on the Actor's Use Scrapely browser setting, which is off by default and applied per build. See Scrape.ly Browser.
| Situation | What happens |
|---|---|
Setting off (SCRAPELY_BROWSER_PATCH=0) | Browsers launch in the container, as in plain Crawlee |
| Setting on, on the platform | Launches connect to the Scrape.ly browser at CDP_URL with connect_over_cdp. Firefox, WebKit, and Camoufox requests go to the same Chromium |
Setting on, no CDP_URL | The launch raises RuntimeError |
Local run, no CDP_URL | Browsers launch locally |
Importing scrapely patches Crawlee's PlaywrightBrowserPlugin, raw Playwright (chromium.launch(), launch_persistent_context()), pyppeteer, and Camoufox. The scrapely/actor-python* images also load the patch through sitecustomize, so scripts that never import the SDK are covered. Your crawler code doesn't change:
from crawlee.crawlers import PlaywrightCrawler
crawler = PlaywrightCrawler(request_handler=handle_page)
await crawler.run(["https://example.com"])set_cdp_url(url) overrides CDP_URL. The SDK reads the URL lazily at each browser launch.
Browser location and fingerprint
These options apply when the browser setting is on. The Scrape.ly browser has its own proxy and ignores custom proxies: a BYOP proxy_configuration passed to a browser crawler is ignored, with a warning. To choose the exit location and fingerprint, pass a BrowserConfiguration:
browser_configuration = await Actor.create_browser_configuration(country_code="US", state="California")
crawler = PlaywrightCrawler(
browser_configuration=browser_configuration,
request_handler=handle_page,
)Actor.create_browser_configuration(actor_browser_input=None, *, country_code=None, state=None, city=None, os=None, timezone=None, language=None, adblock=None, solve_cloudflare=None) is async. It fills country_code, state and city from the input's browserCountry, browserState and browserCity fields when you don't pass them. You can also construct BrowserConfiguration(...) with the same keyword arguments.
| Option | Format |
|---|---|
country_code | ISO 3166-1 alpha-2, such as US |
state | Region name, such as California, not an ISO code |
city | City name, such as San Jose |
os | windows, macos or android |
timezone | IANA timezone, such as Asia/Tokyo. Defaults to the exit country's main zone |
language | Comma-separated tags without q-values, such as de-CH,de |
adblock | Block ads and trackers. Off by default |
solve_cloudflare | Solve Cloudflare Turnstile automatically. On by default |
Invalid values raise ValueError. The browser's location comes from the first source that sets one: browser_configuration, then the country and subdivision of a platform ProxyConfiguration, then the run's default. The first page pins a browser's location. If a later page on the same browser resolves to a different location, the SDK logs a warning. Off the platform, and in builds with the setting off, the option is accepted and ignored.
PlaywrightCrawler(browser_configuration=...) only works because the SDK patches it, and passing any other type raises TypeError. Raw chromium.launch(browser_configuration=...) works too.
Proxy configuration
Actor.create_proxy_configuration() builds a ProxyConfiguration for HTTP crawlers. It supports three modes:
- Platform proxy. The Scrape.ly proxy gateway, available in platform runs when the platform sets
SCRAPELY_PROXY_*. - Your own proxies.
proxy_urls,new_url_functionor tiered lists. - Mixed. Your own lists with an
"auto"(or"http://auto") entry that stands for the platform proxy.
For plans, groups and locations, see Proxy.
# Platform proxy, optionally with groups and location
proxy_configuration = await Actor.create_proxy_configuration(country_code="US")
# Your own proxies
proxy_configuration = await Actor.create_proxy_configuration(
proxy_urls=["http://user:pass@proxy-a.example.com:8000", "http://proxy-b.example.com:8000"],
)
# From a proxy editor field in the input
actor_input = await Actor.get_input() or {}
proxy_configuration = await Actor.create_proxy_configuration(actor_input.get("proxyConfiguration"))
crawler = BeautifulSoupCrawler(proxy_configuration=proxy_configuration, request_handler=handler)await Actor.create_proxy_configuration(
actor_proxy_input=None, password=None, groups=None, country_code=None, subdivision_code=None,
proxy_urls=None, new_url_function=None, tiered_proxy_urls=None, tiered_proxy_config=None,
check_access=True,
)The method needs an initialized SDK and returns None in two cases:
- The input has
useScrapelyProxy: falseand doesn't list any custom proxies. - Nothing is configured and there are no platform proxy credentials. This happens, for example, off the platform, and the SDK logs a warning.
actor_proxy_input accepts the keys useScrapelyProxy, scrapelyProxyGroups, scrapelyProxyCountry, scrapelyProxySubdivision, proxyUrls, tieredProxyUrls and tieredProxyConfig. It also accepts the Apify aliases useApifyProxy, apifyProxyGroups, apifyProxyCountry and apifyProxySubdivision. When both names are set, the Scrapely name wins. Explicit arguments win over the input.
Actor.get_input() returns proxy objects under both names, so actor_input["proxyConfiguration"]["useScrapelyProxy"] and ["useApifyProxy"] always agree. See Apify names.
With check_access=True, the SDK makes a request through the gateway to the proxy status URL: 2 attempts, with a 4-second timeout each. When the proxy refuses the configuration, for example a group other than RESIDENTIAL or DATACENTER, it raises ConnectionError with the proxy's message, such as Proxy group "GOOGLE_SERP" is not available on Scrapely. Available groups: RESIDENTIAL, DATACENTER. See Unsupported groups. When the check gets no answer or an HTTP error, it only logs a warning.
Validation
These combinations raise ValueError when the configuration is created:
- A
country_codethat isn't two uppercase letters. - A
subdivision_codethat isn't 1 to 3 uppercase letters or digits, or that has nocountry_code. - Real custom URLs, or
new_url_function, combined withgroups,country_code,subdivision_codeorpassword. - A
tiered_proxy_configtier that setsnew_url_function. - Needing the platform proxy (no custom proxies, or an
"auto"entry) when there are no platform proxy credentials.
Using a ProxyConfiguration
url = await proxy_configuration.new_url(session_id="session_1")
info = await proxy_configuration.new_proxy_info(session_id="session_1")new_url(session_id=None, request=None, proxy_tier=None) returns the next URL, rotating round-robin. A session ID, at most 50 characters from [a-zA-Z0-9._~], sticks to one URL within each tier. Platform URLs have the form http://<username>,groups-A+B,session-X,country-US_CA:<password>@<host>:<port>. new_proxy_info() returns a ProxyInfo with url, scheme, hostname, port, username, password, session_id, proxy_tier, groups, country_code and subdivision_code.
Serving HTTP
A Python Actor can run its own HTTP server on ACTOR_WEB_SERVER_PORT (4321). The platform exposes that port at ACTOR_WEB_SERVER_URL during a normal run, and it sends standby traffic to it for standby runs. The Python SDK has no standby helper server, so you start the server yourself. Actor.is_standby() tells you which mode you're in.
import asyncio
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from scrapely import Actor
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"ok")
async def main():
async with Actor:
port = Actor.get_env().container_port or 4321
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
Actor.log.info("Listening on %s (standby=%s)", port, Actor.is_standby())
await asyncio.to_thread(server.serve_forever)
if __name__ == "__main__":
asyncio.run(main())Bind to 0.0.0.0, not 127.0.0.1. The JavaScript SDK ships a standby helper. See Standby.