Events and state persistence
Crawlee components talk to each other through events. An event manager emits events such as "save your state now" or "here's the current CPU and memory usage", and any component can register a listener for them. The crawler doesn't need to know which components care about an event. The Snapshotter listens to system info events to drive autoscaling, and statistics, storages, and your own code listen to persist state events to save their progress.
Event manager
The EventManager class keeps a registry of listeners and invokes them whenever an event is emitted. It also emits the PERSIST_STATE event on its own at a regular interval.
Locally, Crawlee uses the LocalEventManager subclass, which additionally measures CPU and memory usage and emits it as the SYSTEM_INFO event. The intervals come from the Configuration:
persist_state_intervaldefaults to 1 minute. You can set it with theCRAWLEE_PERSIST_STATE_INTERVAL_MILLISenvironment variable.system_info_intervaldefaults to 1 second. You can set it with theCRAWLEE_SYSTEM_INFO_INTERVAL_MILLISenvironment variable.
The event manager is one of the global services of the service locator. Call service_locator.get_event_manager() to get it. If none was set, the service locator creates a LocalEventManager from the current configuration on first access. To use a different one, register it with service_locator.set_event_manager() before you create the crawler. A crawler also accepts an event_manager argument, but components such as statistics and storages subscribe through the global service locator, so the global registration is the one to change.
The event manager is an async context manager. The crawler enters it when run() starts and exits it when the run finishes, so events are only emitted while a crawler runs. Outside a crawler, use async with event_manager: yourself.
Events
The Event enum lists the event names. Each event comes with a payload model:
PERSIST_STATEcarriesEventPersistStateData. It's emitted when the event manager starts, then at everypersist_state_interval, and one last time when it closes. Itsis_migratingflag tells whether the process is about to be migrated.SYSTEM_INFOcarriesEventSystemInfoDatawith the CPU usage ratio and the memory used by the current process and its children. TheLocalEventManageremits it at everysystem_info_interval.CRAWLER_STATUScarriesEventCrawlerStatusDatawith the same status message that the crawler periodically logs.MIGRATING,ABORTING, andEXITcarryEventMigratingData,EventAbortingData, andEventExitData. Crawlee itself never emits them locally. They come from a platform, for example from the event manager of the Apify SDK when an Actor run is migrated to another server or aborted.
The enum also defines session pool and browser pool event names, such as SESSION_RETIRED or BROWSER_LAUNCHED, which Crawlee for Python doesn't emit.
Listening to events
Register a listener with on() and remove it with off(). Calling off() without a listener removes all listeners of the event. A listener can be sync or async and accepts either the event payload or no argument at all. Each invocation runs in its own task, and sync listeners run in a separate thread, so a slow listener doesn't block the crawler. An exception raised by a listener is logged and doesn't affect other listeners.
The following example registers an event manager with a shorter persist state interval and prints the payload of the persist state and system info events during a short crawl:
import asyncio
from datetime import timedelta
from crawlee import service_locator
from crawlee.crawlers import ParselCrawler, ParselCrawlingContext
from crawlee.events import (
Event,
EventPersistStateData,
EventSystemInfoData,
LocalEventManager,
)
async def main() -> None:
# Register an event manager with a shorter persist state interval, so that
# the event fires several times during this short crawl.
event_manager = LocalEventManager(persist_state_interval=timedelta(seconds=2))
service_locator.set_event_manager(event_manager)
async def on_persist_state(event_data: EventPersistStateData) -> None:
print(f'Persist state event (is_migrating={event_data.is_migrating})')
async def on_system_info(event_data: EventSystemInfoData) -> None:
cpu = event_data.cpu_info.used_ratio
memory = event_data.memory_info.current_size
print(f'System info event: CPU {cpu:.0%}, memory {memory}')
event_manager.on(event=Event.PERSIST_STATE, listener=on_persist_state)
event_manager.on(event=Event.SYSTEM_INFO, listener=on_system_info)
crawler = ParselCrawler(max_requests_per_crawl=20)
@crawler.router.default_handler
async def request_handler(context: ParselCrawlingContext) -> None:
context.log.info(f'Processing {context.request.url} ...')
await context.enqueue_links()
await crawler.run(['https://crawlee.dev'])
# Stop listening to the system info events.
event_manager.off(event=Event.SYSTEM_INFO, listener=on_system_info)
if __name__ == '__main__':
asyncio.run(main())
State persistence
State persistence builds on the PERSIST_STATE event. Components that need to survive a restart or a migration keep their state in memory, save it to a KeyValueStore whenever the event fires, and load it back on the next start. The default key-value store of a crawler run then holds records like these:
__CRAWLER_STATISTICS_<id>holds the crawler's statistics. The crawler's defaultStatisticsinstance has persistence enabled.__RQ_STATE_<id>holds the ordering and in-progress information of a request queue backed by the file system storage client. The requests themselves are stored as individual files.CRAWLEE_STATE_<id>holds the state you manage withuse_state, as described in Crawler state.
Other components persist their state when you ask them to. The SessionPool does so with persistence_enabled=True, and the RequestList with a persist_state_key. For details, see Request loaders.
Internally, these components use a helper class that loads the state on start, subscribes to PERSIST_STATE, and saves the state one last time on teardown. The helper isn't part of the public API. In your own code, use use_state, or listen to PERSIST_STATE and save your data to a key-value store.
Persisted state only helps if it's still there on the next run. Crawlee purges the default storages on start, so to resume an interrupted crawl, disable purge_on_start. For details, see Stopping and resuming crawlers.
Crawler state
The use_state helper of the crawling context returns a dictionary that's saved automatically. Every call within a crawler returns the same dictionary, so you can read and update it from any request handler. Crawlee saves it to the default key-value store on every PERSIST_STATE event and when the run finishes, and loads it back from there when the crawler starts again with existing storage. The optional argument sets the initial value when no saved state exists yet.
The dictionary is stored under the CRAWLEE_STATE_<id> key, where the id is the crawler's id argument or, without one, a per-process counter of crawler instances. Pass the same id to the constructors of several crawlers to make them share their state. Outside request handlers, use crawler.use_state(). Values have to be JSON serializable.
The following example stores the page title of every visited URL in the crawler state and reads it after the run:
import asyncio
from crawlee.crawlers import ParselCrawler, ParselCrawlingContext
async def main() -> None:
crawler = ParselCrawler(max_requests_per_crawl=10)
@crawler.router.default_handler
async def request_handler(context: ParselCrawlingContext) -> None:
# Every call returns the same dictionary. Crawlee saves it to the default
# key-value store on each persist state event and at the end of the run.
state = await context.use_state()
state[context.request.url] = context.selector.css('title::text').get()
await context.enqueue_links()
await crawler.run(['https://crawlee.dev'])
# The crawler exposes the same state outside the request handlers.
state = await crawler.use_state()
print(f'Title of the start page: {state["https://crawlee.dev"]}')
if __name__ == '__main__':
asyncio.run(main())
use_state is an experimental feature, so its behavior and interface may change in future versions.