Skip to content
Python SDK

Events & state

Platform events, state persistence, reboot, and status messages.

The platform sends events to a running Actor over a WebSocket. These events report resource usage, tell the Actor to save its state, and warn it before a migration or an abort. This page covers subscribing to events, persisting state with Actor.use_state(), rebooting after a migration and setting the run's status message.

Subscribing to events

Use Actor.on() to subscribe and Actor.off() to unsubscribe. Both are synchronous and need an initialized SDK. Events are members of Crawlee's Event enum, which scrapely re-exports.

.python
import asyncio

from scrapely import Actor, Event


async def on_aborting() -> None:
    await asyncio.sleep(1)
    await Actor.exit()


async def main() -> None:
    async with Actor:
        Actor.on(Event.ABORTING, on_aborting)
        ...

A listener can take no arguments or one argument, the event data, and it can be sync or async. Actor.off(event) without a listener removes every listener for that event.

Event names

MemberValueWhen Actor.on listeners receive it
Event.PERSIST_STATEpersistStateEvery persist_state_interval_millis (60 s by default), when the platform sends it, on migrating, on Actor.reboot() and at shutdown
Event.SYSTEM_INFOsystemInfoWhen the platform sends memory and CPU samples. Off the platform, every second
Event.MIGRATINGmigratingThe run is about to move to another server
Event.ABORTINGabortingA user or the API requested a graceful abort. The platform sends persistState first
Event.EXITexitDefined for compatibility. Scrape.ly doesn't send it

PERSIST_STATE data has an is_migrating flag. SYSTEM_INFO data is Crawlee's EventSystemInfoData. The platform's deprecated cpuInfo message and any unknown messages are ignored.

Crawlee defines more events, such as SESSION_RETIRED, BROWSER_LAUNCHED, BROWSER_RETIRED, BROWSER_CLOSED, PAGE_CREATED, PAGE_CLOSED and CRAWLER_STATUS. Crawlers emit these on Crawlee's own event manager.

No automatic shutdown handlers

The SDK doesn't exit on aborting or reboot on migrating by itself. InitOptions.graceful_shutdown is accepted but not used. If you want those behaviours, register the listeners, as the official templates do for aborting.

If the container gets SIGTERM or SIGINT, the SDK runs a bounded shutdown and exits with code 0. See Signals.

Connection

When ACTOR_EVENTS_WEBSOCKET_URL is set, ScrapelyEventManager connects to it with the run token in the background and reconnects with backoff. A failed connection never blocks Actor.init(). Without the URL, the SDK uses Crawlee's LocalEventManager.

State persistence

A run can move to another server, and anything held only in memory is lost. Keep state that must survive in a key-value store.

Actor.use_state()

.python
state = await Actor.use_state(default_value={"processed": 0, "cursor": None})
state["processed"] += 1

use_state(default_value=None, key=None, kvs_name=None) returns a mutable dict backed by a key-value store record. The default key is APIFY_GLOBAL_STATE, and the default store is the run's default key-value store. Change the dict in place. Rebinding the name (state = {...}) doesn't change what gets saved.

The dict is saved whenever Crawlee's global event manager emits persistState. That happens about every minute while a crawler runs, and once when the crawler finishes. If no crawler is running, or if you need a save at a specific moment, save the dict explicitly:

.python
store = await Actor.open_key_value_store()          # pass name=... if you used kvs_name
await store.persist_autosaved_values()

To save state before a migration, forward the platform event:

.python
async def on_migrating(event_data) -> None:
    store = await Actor.open_key_value_store()
    await store.persist_autosaved_values()
    await Actor.reboot()


Actor.on(Event.MIGRATING, on_migrating)

After the restart, use_state() loads the saved value, so the Actor resumes where it stopped.

.python
state = await Actor.use_state(default_value={"cursor": 0}, key="crawl-state", kvs_name="state-store")

Actor.reboot()

.python
await Actor.reboot(options=None)

reboot() restarts the run in a fresh container with the same run ID and the same default storages. It runs these steps in order:

  1. Emits PERSIST_STATE with is_migrating=True to Actor.on() listeners.
  2. Waits one second.
  3. Calls POST /v2/actor-runs/{runId}/reboot.
  4. Waits for the platform to stop the container. The wait is RebootOptions.custom_after_sleep_millis, or metamorph_after_sleep_millis (300000 ms by default).
  5. If the container is still alive after the wait, flushes pending charges and calls sys.exit(0).

Off the platform, reboot() logs a warning and returns. Without a run ID, it raises RuntimeError.

Status messages

.python
from scrapely import Actor, SetStatusMessageOptions

await Actor.set_status_message("Downloading page 3")
await Actor.set_status_message("Completed", is_terminal=True)
await Actor.set_status_message(
    "Retrying upstream",
    SetStatusMessageOptions(level="WARNING"),
)

set_status_message(message, options=None, *, is_terminal=None) first logs [Status message]: <message> at options.level (DEBUG, INFO, WARNING or ERROR). It then sends PUT /v2/actor-runs/{runId} with statusMessage and isStatusMessageTerminal, and returns the updated ActorRun. Without API credentials, it only logs, and it returns an empty ActorRun.

is_terminal overrides SetStatusMessageOptions.is_status_message_terminal. A terminal message is meant to be the run's final message.

Actor.status_message is a separate value. It holds the message passed to exit() or fail(). On the platform, exit() and fail() also save that message on the run as a terminal message.

What's next