Skip to main content
Version: Next

Storages

Crawlee offers several storage types for managing and persisting your crawling data. Request-oriented storages, such as the RequestQueue, help you store and deduplicate URLs, while result-oriented storages, like Dataset and KeyValueStore, focus on storing and retrieving scraping results. This page explains when to use each type, how to interact with them, and how to control their lifecycle.

Overview​

Crawlee's storage system consists of two main layers:

For more information about storage clients and their configuration, see the Storage clients page.

Named and unnamed storages​

Crawlee supports two types of storages:

  • Named storages: Persistent storages with a specific name that persist across runs. These are useful when you want to share data between different crawler runs or access the same storage from multiple places.
  • Unnamed storages: Temporary storages identified by an alias that are scoped to a single run. These are automatically purged at the start of each run (when purge_on_start is enabled, which is the default).

Default storage​

Each storage type (Dataset, KeyValueStore, RequestQueue) has a default instance that can be accessed without specifying id, name or alias. Default unnamed storage is accessed by calling storage's open method without parameters. This is the most common way to use storages in simple crawlers.

Run on
import asyncio

from crawlee.storages import Dataset


async def main() -> None:
# Named storage (persists across runs)
dataset_named = await Dataset.open(name='my-persistent-dataset')

# Unnamed storage with alias (purged on start)
dataset_unnamed = await Dataset.open(alias='temporary-results')

# Default unnamed storage (purged on start)
dataset_default = await Dataset.open()


if __name__ == '__main__':
asyncio.run(main())

Request queue​

The RequestQueue is the primary storage for URLs in Crawlee, especially useful for deep crawling. It supports dynamic addition of URLs, making it ideal for recursive tasks where URLs are discovered and added during the crawling process (e.g., following links across multiple pages). Each Crawlee project has a default request queue, which can be used to store URLs during a specific run. For how the queue deduplicates requests, see Requests.

The following code demonstrates the usage of the RequestQueue:

Run on
import asyncio

from crawlee.storages import RequestQueue


async def main() -> None:
# Open the request queue, if it does not exist, it will be created.
# Leave name empty to use the default request queue.
request_queue = await RequestQueue.open(name='my-request-queue')

# Add a single request.
await request_queue.add_request('https://apify.com/')

# Add multiple requests as a batch.
await request_queue.add_requests(
['https://crawlee.dev/', 'https://crawlee.dev/python/']
)

# Fetch and process requests from the queue.
while request := await request_queue.fetch_next_request():
# Do something with it...

# And mark it as handled.
await request_queue.mark_request_as_handled(request)

# Remove the request queue.
await request_queue.drop()


if __name__ == '__main__':
asyncio.run(main())

Crawlee provides helper functions to simplify interactions with the RequestQueue. Requests and data added through the context helpers are written only after the request handler succeeds. For details, see Crawling context. The helpers are:

  • The add_requests function allows you to manually add specific URLs to the configured request storage. In this case, you must explicitly provide the URLs you want to be added to the request storage. If you need to specify further details of the request, such as a label or user_data, you have to pass instances of the Request class to the helper.
  • The enqueue_links function is designed to discover new URLs in the current page and add them to the request storage. It can be used with default settings, requiring no arguments, or you can customize its behavior by specifying link element selectors, choosing different enqueue strategies, or applying include/exclude filters to control which URLs are added. See the Enqueue strategies section of the Crawling links guide for more details.
Run on
import asyncio

from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext


async def main() -> None:
crawler = BeautifulSoupCrawler()

@crawler.router.default_handler
async def request_handler(context: BeautifulSoupCrawlingContext) -> None:
context.log.info(f'Processing {context.request.url} ...')
await context.add_requests(['https://apify.com/'])

await crawler.run(['https://crawlee.dev/'])


if __name__ == '__main__':
asyncio.run(main())

Request manager​

The RequestQueue implements the RequestManager interface, offering a unified API for interacting with various request storage types. This provides a unified way to interact with different request storage types.

If you need custom functionality, you can create your own request storage by subclassing the RequestManager class and implementing its required methods.

For a detailed explanation of the RequestManager and other related components, refer to the Request loaders page.

Dataset​

The Dataset is designed for storing structured data, where each entry has a consistent set of attributes, such as products in an online store or real estate listings. Think of a Dataset as a table: each entry corresponds to a row, with attributes represented as columns. Datasets are append-only, allowing you to add new records but not modify or delete existing ones. Every Crawlee project run is associated with a default dataset, typically used to store results specific to that crawler execution. However, using this dataset is optional.

The following code demonstrates basic operations of the dataset:

Run on
import asyncio

from crawlee.storages import Dataset


async def main() -> None:
# Open the dataset, if it does not exist, it will be created.
# Leave name empty to use the default dataset.
dataset = await Dataset.open(name='my-dataset')

# Push a single row of data.
await dataset.push_data({'foo': 'bar'})

# Push multiple rows of data (anything JSON-serializable can be pushed).
await dataset.push_data([{'foo': 'bar2', 'col2': 'val2'}, {'col3': 123}])

# Fetch all data from the dataset.
data = await dataset.get_data()
# Do something with it...

# Remove the dataset.
await dataset.drop()


if __name__ == '__main__':
asyncio.run(main())

Crawlee provides the following helper function to simplify interactions with the Dataset:

  • The push_data function allows you to manually add data to the dataset. You can optionally specify the dataset ID or its name.

Exporting the dataset​

The BasicCrawler.export_data method exports the entire default dataset to a single file, in either CSV or JSON format. It also accepts additional keyword arguments so you can fine-tune the underlying json.dump or csv.DictWriter behavior. The method is available on all crawlers.

Run on
import asyncio

from crawlee.crawlers import BeautifulSoupCrawler, BeautifulSoupCrawlingContext


async def main() -> None:
crawler = BeautifulSoupCrawler(
# Limit the crawl to max requests. Remove or increase it for crawling all links.
max_requests_per_crawl=10,
)

# Define the default request handler, which will be called for every request.
@crawler.router.default_handler
async def request_handler(context: BeautifulSoupCrawlingContext) -> None:
context.log.info(f'Processing {context.request.url} ...')

# Extract data from the page.
data = {
'url': context.request.url,
'title': context.soup.title.string if context.soup.title else None,
}

# Enqueue all links found on the page.
await context.enqueue_links()

# Push the extracted data to the default dataset.
await context.push_data(data)

# Run the crawler with the initial list of URLs.
await crawler.run(['https://crawlee.dev'])

# Export the entire dataset to a JSON file.
# Set ensure_ascii=False to allow Unicode characters in the output.
await crawler.export_data(path='results.json', ensure_ascii=False)


if __name__ == '__main__':
asyncio.run(main())

Dataset items don't have to share a schema. Different handlers can push different fields, so the items of one dataset often have different keys. By default, the CSV columns are the keys of the first non-empty item. When a later item has a key the first one doesn't, its value isn't written, and Crawlee logs a warning naming the dropped keys. To use the keys of all items as columns instead, pass collect_all_keys=True:

await crawler.export_data(path='results.csv', collect_all_keys=True)

No value is dropped in that mode. Collecting all keys means reading the whole dataset before the first row can be written, so only the default mode writes rows as it goes. In both modes, cells for columns an item doesn't have stay empty, or hold the restval value if you pass one.

Key-value store​

The KeyValueStore is designed to save and retrieve data records or files efficiently. Each record is uniquely identified by a key and is associated with a specific MIME type, making the KeyValueStore ideal for tasks like saving web page screenshots, PDFs, or tracking the state of crawlers.

The following code demonstrates the usage of the KeyValueStore:

Run on
import asyncio

from crawlee.storages import KeyValueStore


async def main() -> None:
# Open the key-value store, if it does not exist, it will be created.
# Leave name empty to use the default KVS.
kvs = await KeyValueStore.open(name='my-key-value-store')

# Set a value associated with 'some-key'.
await kvs.set_value(key='some-key', value={'foo': 'bar'})

# Get the value associated with 'some-key'.
value = kvs.get_value('some-key')
# Do something with it...

# Delete the value associated with 'some-key' by setting it to None.
await kvs.set_value(key='some-key', value=None)

# Remove the key-value store.
await kvs.drop()


if __name__ == '__main__':
asyncio.run(main())

To see a real-world example of storing files in the key-value store, see the Capturing screenshots example.

Crawlee provides the following helper function to simplify interactions with the KeyValueStore:

  • The get_key_value_store function retrieves the key-value store for the current crawler run. If the KVS does not exist, it will be created. You can also specify the KVS's ID or its name.

Cleaning up the storages​

By default, Crawlee cleans up all unnamed storages (including the default one) at the start of each run, so every crawl begins with a clean state. This behavior is controlled by Configuration.purge_on_start (default: True). In contrast, named storages are never purged automatically and persist across runs. The exact behavior may vary depending on the storage client implementation.

When purging happens​

The cleanup occurs as soon as a storage is accessed:

Disabling automatic purging​

To disable automatic purging, set purge_on_start=False in your configuration:

Run on
import asyncio

from crawlee.configuration import Configuration
from crawlee.crawlers import HttpCrawler, HttpCrawlingContext


async def main() -> None:
# Set the purge_on_start field to False to avoid purging the storage on start.
configuration = Configuration(purge_on_start=False)

# Pass the configuration to the crawler.
crawler = HttpCrawler(configuration=configuration)

@crawler.router.default_handler
async def request_handler(context: HttpCrawlingContext) -> None:
context.log.info(f'Processing {context.request.url} ...')

await crawler.run(['https://crawlee.dev/'])


if __name__ == '__main__':
asyncio.run(main())

For setting purge_on_start and storage_dir in code or through environment variables, see Configuration.

Manual purging​

Purge on start behavior just triggers the storage's purge method, which removes all data from the storage. If you want to purge the storage manually, you can do so by calling the purge method on the storage instance. Or if you want to delete the storage completely, you can call the drop method on the storage instance, which will remove the storage, including metadata and all its data.

Run on
import asyncio

from crawlee.storages import Dataset


async def main() -> None:
# Create storage client with configuration
dataset = await Dataset.open(name='my-dataset')

# Purge the dataset explicitly - purging will remove all items from the dataset.
# But keeps the dataset itself and its metadata.
await dataset.purge()

# Or you can drop the dataset completely, which will remove the dataset
# and all its items.
await dataset.drop()


if __name__ == '__main__':
asyncio.run(main())

Note that purging behavior may vary between storage client implementations. For more details on storage configuration and client implementations, see the Storage clients page.