Optimization
A crawl goes as fast as its slowest part allows. Find out which part that is before changing any setting.
Broad crawls have their own set of recommended adjustments.
Finding the bottleneck
The bottleneck depends on the spider: on the same machine, one crawl can be limited by its own parsing code and another by the target website. So measure the crawl that you want to optimize.
LogStats reports crawl speed every
LOGSTATS_INTERVAL seconds:
[scrapy.extensions.logstats] INFO: Crawled 1200 pages (at 60 pages/min), scraped 1150 items (at 58 items/min)
A rate that stays flat as you raise CONCURRENT_REQUESTS means
something else is the limit.
Reading the engine status
The telnet console reports, through est(),
what every part of the engine is doing at a given moment:
len(engine.downloader.active) : 16
len(engine._slot.scheduler.mqs) : 92
len(engine.scraper.slot.active) : 0
engine.scraper.slot.active_size : 0
engine.scraper.slot.needs_backout() : False
Take a few readings at different points of the crawl:
len(engine.downloader.active)stays atCONCURRENT_REQUESTS: the downloader is the limit. You are waiting on the network or on the target website. See Sending more requests at a time.len(engine.downloader.active)stays belowCONCURRENT_REQUESTSwhile the scheduler queues (mqs,dqs) hold requests: something throttles those requests before they reach the downloader, usuallyCONCURRENT_REQUESTS_PER_DOMAIN,DOWNLOAD_DELAYor AutoThrottle.Both the downloader and the scheduler queues stay near empty: your spider is not producing requests fast enough. A crawl that walks pagination one page at a time cannot use more concurrency than it creates. See Producing requests faster.
needs_backout()isTrue, oractive_sizeapproachesSCRAPER_SLOT_MAX_ACTIVE_SIZE: responses arrive faster than your callbacks and item pipelines handle them. The bottleneck is your own code.len(engine._slot.scheduler.mqs)grows without settling: the crawl discovers requests faster than it downloads them. This is what makes long crawls run out of memory.
Reading resource usage
- CPU
Scrapy runs in a single process, and everything except DNS resolution and code you explicitly move to a thread runs in a single thread. One CPU core is the ceiling; a process sitting at 100% of a core is CPU-bound no matter how many cores the machine has.
Use a sampling profiler, such as py-spy, to find out which code is spending that CPU. Selectors and item pipelines are the usual answer.
- Memory
The memory usage extension records
memusage/startupandmemusage/max. Amemusage/maxfar abovememusage/startupis expected; what matters is whether it keeps growing for as long as the crawl runs.Growth that tracks
len(engine._slot.scheduler.mqs)is a scheduling problem, covered in Lowering memory usage. Growth that does not is a memory leak.- Network
Compare
downloader/response_bytesover the crawl time against your available bandwidth. Saturated bandwidth caps concurrency regardless of any setting.DNS resolution is separate: it runs on a thread pool of
REACTOR_THREADPOOL_MAXSIZEthreads, and results are cached (DNSCACHE_ENABLED,DNSCACHE_SIZE). It only becomes a limit of its own when there are many different domains to resolve, as in broad crawls, where it shows up as slow starts and DNS timeouts.- Disk
Feed exports write to disk on most crawls, although item data is usually small enough for that not to matter. The ones to suspect are
HttpCacheMiddlewareand the media pipelines, which write whole responses, andJOBDIR, which writes every scheduled request.
Sending more requests at a time
CONCURRENT_REQUESTS caps how many requests are being downloaded at
any given moment, CONCURRENT_REQUESTS_PER_DOMAIN caps how many of
those may target the same domain, and DOWNLOAD_DELAY sets a minimum
wait between two consecutive requests to the same domain. A project generated by
startproject gets one request per second per domain out of these.
Raise them to crawl a single website faster, and see broad-crawls-concurrency to spread requests across many websites instead.
The limit that matters, though, is the one the target website tolerates. Exceeding it gets you throttled, served errors or banned, all of which make the crawl slower than a lower concurrency would have been. To find that limit:
Read the robots.txt file of the website. Scrapy does not act on its
Crawl-delayandRequest-ratedirectives, so when they are present, translate them intoDOWNLOAD_DELAYand concurrency settings yourself.Check the traffic that the website already gets, using a service like SimilarWeb or Cloudflare Radar. A rate that is a rounding error next to what the website serves anyway is unlikely to be a problem for it.
Look for a documented way in. An API, a bulk export or a search endpoint is both faster for you and cheaper for the website than crawling its pages, and the terms of service may state a rate.
Crawl when the website is idle, in its own timezone, so that the capacity you take is capacity nobody else wanted.
Raise concurrency gradually and watch the website respond.
downloader/response_status_count/{status_code}counts for 429, 503 or the ban page of the website, growingretry/count, or a download latency that climbs as you push harder, all mean you have gone past the limit.
Producing requests faster
A spider that discovers its requests one response at a time keeps the
downloader idle no matter how high you set CONCURRENT_REQUESTS. To
put more requests in the scheduler earlier:
Request every page at once when you can work out how many there are, e.g. from a page count or from a result count and a page size in the first response, instead of following a link to the next page on every response.
Get URLs from a source that lists many of them at once, such as a sitemap or a search or export endpoint of the target website. For a crawl that needs nothing else,
SitemapSpiderreads sitemaps for you.Raise the
priorityof pagination requests, so that they are downloaded before the requests that they compete with, and discover the rest of the crawl sooner.
Each of these trades memory for speed: a request produced before the downloader
can take it waits in the scheduler, or on disk if you set JOBDIR.
Pushed far enough, they turn memory or disk into your new bottleneck, which is
why Lowering memory usage recommends the reverse of the last point.
Lowering resource usage
Lowering memory usage
Lower
SCRAPER_SLOT_MAX_ACTIVE_SIZE.Lower
DOWNLOAD_MAXSIZE, which allows a single response to take up to 1 GiB of memory by default, multiplied by your concurrency. SetDOWNLOAD_WARNSIZEfirst to find out whether the website actually serves responses that big.Lower the number of scheduled requests held in memory:
Increase the
priorityof requests whosecallbackcannot yield additional requests.For example, the following spider uses a higher priority (1) for book requests than for pagination requests:
from scrapy import Spider class BooksToScrapeComSpider(Spider): name = "books_toscrape_com" start_urls = [ "http://books.toscrape.com/catalogue/category/books/mystery_3/index.html" ] def parse(self, response): next_page_links = response.css(".next a") yield from response.follow_all(next_page_links) book_links = response.css("article a") yield from response.follow_all(book_links, callback=self.parse_book, priority=1) def parse_book(self, response): yield { "name": response.css("h1::text").get(), "price": response.css(".price_color::text").re_first("£(.*)"), "url": response.url, }
Note
If the number of request-yielding, low-priority requests scheduled at any given time is lower than concurrency settings (
CONCURRENT_REQUESTS_PER_DOMAINorCONCURRENT_REQUESTS), as in the example above, this can slow down your crawl by turning those requests into a bottleneck.If you have many start requests, consider delaying their iteration.
Set
JOBDIRto offload all scheduled requests to disk.
Be on the lookout for memory leaks.
Lowering network usage
Enable
HttpCacheMiddlewarewhile developing your spider, so that re-runs do not download the same responses again.
Lowering CPU usage
Other tips
Try using the asyncio reactor with uvloop as custom event loop, i.e. setting
ASYNCIO_EVENT_LOOPto"uvloop.Loop".Alternatively, try switching to a non-asyncio reactor.
Disable unused components.
For example, set
COOKIES_ENABLEDtoFalseunless you need cookies.Split the crawl across separate processes to use more than one CPU core. See Distributed crawls.
Speeding up broad crawls
While Scrapy is well suited for broad crawls, i.e. crawls that target many websites, the default settings are optimized for crawls targeting a single website.
For broad crawls, consider these adjustments:
Increase the global concurrency:
Set
CONCURRENT_REQUESTSas close toCONCURRENT_REQUESTS_PER_DOMAIN× [number of target domains] (e.g. 8 × 10 domains = 80 concurrent requests) as your CPU and memory allow.Increase
SCRAPER_SLOT_MAX_ACTIVE_SIZEwhen increasingCONCURRENT_REQUESTSstops making a difference.
If memory is a bottleneck, see if crawling in BFO order lowers memory usage.
Improve DNS resolution speed:
Set up your own DNS server, with a local cache and upstream to a large DNS server, to avoid slowing down your network.
Increase
REACTOR_THREADPOOL_MAXSIZEto the minimum value that avoids DNS resolution timeouts and makes a noticeable positive impact in crawl speed.
Lower the negative impact of some responses:
Set
RETRY_ENABLEDtoFalseor, if you need retries, consider loweringRETRY_TIMES.Lower
DOWNLOAD_TIMEOUTto a more reasonable value, to discard stuck requests more quickly.Set
REDIRECT_ENABLEDtoFalseunless you want to follow redirects.