Broad crawls and scaling out

Scrapy · Expert · 12 min

Crawling thousands of sites is a different workload from scraping one, and defaults tuned for focused crawls hold it back.

A broad crawl visits many domains, often without knowing them in advance. Scrapy's defaults are tuned for crawling a single site, so the bottlenecks move from politeness to throughput and memory. The optimization guide recommends:

  • Raise global concurrency: set CONCURRENT_REQUESTS close to CONCURRENT_REQUESTS_PER_DOMAIN times the number of domains in flight, as CPU and memory allow, and raise SCRAPER_SLOT_MAX_ACTIVE_SIZE when more concurrency stops helping.
  • Keep the scheduler domain-aware: SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.DownloaderAwarePriorityQueue" (the default in current versions; older ones needed it set) picks requests for domains with free download slots, so one slow site can't dominate.
  • Speed up DNS with a local caching resolver and a larger REACTOR_THREADPOOL_MAXSIZE, since DNS lookups use threads.
  • Cut the cost of bad responses: disable or reduce retries, lower DOWNLOAD_TIMEOUT, set REDIRECT_ENABLED = False unless you need redirects, and cap sizes with DOWNLOAD_MAXSIZE.
  • Consider breadth-first order, DEPTH_PRIORITY = 1 with FIFO queues (SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue", SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.FifoMemoryQueue"): it reaches many sites shallowly before going deep, and the docs suggest trying it when memory is the bottleneck. JOBDIR keeps the queue on disk.
  • Guard memory and traps with MEMUSAGE_LIMIT_MB and DEPTH_LIMIT (infinite calendars and session-id URLs are classic traps).

When one process isn't enough, scale out: run several Scrapy processes sharing a central queue and dupefilter, typically with the scrapy-redis extension, or partition the URL space (by domain hash) across independent crawlers.

At this scale, politeness is still per site: keep obeying robots.txt and per-domain limits.

Example

# settings.py for a broad crawl
CONCURRENT_REQUESTS = 200                  # ~2 per domain x 100 domains
CONCURRENT_REQUESTS_PER_DOMAIN = 2
SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.DownloaderAwarePriorityQueue"
REACTOR_THREADPOOL_MAXSIZE = 20

# breadth-first order
DEPTH_PRIORITY = 1
SCHEDULER_DISK_QUEUE = "scrapy.squeues.PickleFifoDiskQueue"
SCHEDULER_MEMORY_QUEUE = "scrapy.squeues.FifoMemoryQueue"
DEPTH_LIMIT = 3

REDIRECT_ENABLED = False
RETRY_ENABLED = False
DOWNLOAD_TIMEOUT = 15
DOWNLOAD_MAXSIZE = 5 * 1024 * 1024     # skip responses over 5 MB
MEMUSAGE_LIMIT_MB = 2048
LOG_LEVEL = "INFO"
ROBOTSTXT_OBEY = True

# $ scrapy crawl broad -s JOBDIR=crawls/broad-1

Remember

  • Raise global concurrency toward per-domain limit x number of domains
  • DownloaderAwarePriorityQueue keeps slow domains from hogging the queue
  • DEPTH_PRIORITY=1 + FIFO queues = breadth-first crawling
  • Scale out with a shared queue and dupefilter (e.g. scrapy-redis)

Watch out for

  • Running a broad crawl with single-site defaults, so it dives deep into the first few sites while most domains wait.

Check yourself

  1. Which settings give breadth-first crawl order?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scrapy.org/en/latest/topics/optimize.html#broad-crawls