scrapy/docs/topics/throttling.rst

1156 lines
40 KiB
ReStructuredText
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

.. _throttling:
==========
Throttling
==========
.. versionadded:: VERSION
Sending too many requests too quickly can `overwhelm websites`_.
:ref:`Throttling <basic-throttling>` and :ref:`backoff <backoff>` aim to
prevent that.
.. _overwhelm websites: https://en.wikipedia.org/wiki/Denial-of-service_attack
.. _basic-throttling:
Concurrency and delay
=====================
Requests are throttled on a **per-domain basis** by default [1]_. This allows
efficient crawling of multiple sites simultaneously.
Each domain and subdomain is treated separately: requests to
``books.toscrape.com`` and ``quotes.toscrape.com`` each have their own
throttling limits, as do ``toscrape.com`` and ``books.toscrape.com``.
The main throttling :ref:`settings <topics-settings>` are:
- .. setting:: CONCURRENT_REQUESTS_PER_DOMAIN
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` (default: ``8``)
Maximum number of simultaneous requests per domain.
It defines a number of “slots” per domain. Each slot can send 1 request at
a time: it sends a request, waits for the response, then sends the next
request, and so on.
- .. setting:: DOWNLOAD_DELAY
:setting:`DOWNLOAD_DELAY` (default: ``1``
(:ref:`fallback <default-settings>`: ``0``))
Minimum seconds between any two requests to the same domain.
To target a specific number of requests per minute (RPM) *per domain*, set
this to ``60 / RPM``. For example, ``DOWNLOAD_DELAY = 1.0`` for 60 RPM, or
``DOWNLOAD_DELAY = 2.0`` for 30 RPM.
When configuring these settings, note that:
- :setting:`CONCURRENT_REQUESTS` caps :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`.
- If ``DOWNLOAD_DELAY`` ≥ response time, concurrency is effectively ``1``,
because the next request to the domain is not sent until the delay elapses,
by which time the previous response has already arrived.
.. [1] You can :ref:`customize <throttling-scopes>` how requests are grouped
for throttling, but domain-based throttling works well in most cases. For
more complex domain grouping strategies, see
:ref:`alternative-domain-throttling`.
.. setting:: THROTTLING_SCOPES
.. _per-domain-throttling:
Per-domain throttling
=====================
The :setting:`THROTTLING_SCOPES` setting allows you to customize throttling
behavior for specific domains [1]_.
It is a dict that maps scope names to
:class:`~scrapy.throttling.ThrottlingScopeConfig` dicts.
Its default value allows faster crawling of the testing website using during
the :ref:`tutorial <intro-tutorial>` while maintaining conservative defaults
for other domains:
.. code-block:: python
THROTTLING_SCOPES = {
"quotes.toscrape.com": {"concurrency": 16, "delay": 0.0},
}
Additional keys like ``"jitter"`` and ``"backoff"`` can be used here and are
covered later on.
.. _backoff:
Backoff
=======
When servers respond with rate limiting errors (like HTTP 429) or network
timeouts occur, request rate is automatically reduced using `exponential
backoff`_.
.. _exponential backoff: https://en.wikipedia.org/wiki/Exponential_backoff
The key settings are:
- .. setting:: BACKOFF_HTTP_CODES
:setting:`BACKOFF_HTTP_CODES` (default: ``[429, 502, 503, 504, 520, 521, 522, 523, 524]``)
HTTP response status codes that trigger backoff.
- .. setting:: BACKOFF_DELAY_FACTOR
:setting:`BACKOFF_DELAY_FACTOR` (default: ``2.0``)
Factor by which the delay of a scope is multiplied on each backoff step
(2×, 4×, 8×, etc.).
- .. setting:: BACKOFF_MAX_DELAY
:setting:`BACKOFF_MAX_DELAY` (default: ``300.0``)
Maximum delay, in seconds, that backoff can reach. Also caps
:ref:`Retry-After <retry-after>` and :ref:`RateLimit-Reset
<rate-limiting-headers>` delays.
See :ref:`throttling-settings` for :setting:`BACKOFF_EXCEPTIONS`,
:setting:`BACKOFF_JITTER`, :setting:`BACKOFF_MIN_DELAY` and
:setting:`BACKOFF_WINDOW`.
.. _backoff-algorithm:
How backoff works
-----------------
Every :ref:`throttling scope <throttling-scopes>` keeps a current delay that
starts at its configured value (``0`` by default, or :setting:`DOWNLOAD_DELAY`
for the default per-domain scopes).
A **backoff trigger** is a response whose status code is in
:setting:`BACKOFF_HTTP_CODES` or a download exception whose type is in
:setting:`BACKOFF_EXCEPTIONS`. On each trigger:
#. If the response carries a :ref:`Retry-After or RateLimit-Reset
<rate-limiting-headers>` value, that value (capped at
:setting:`BACKOFF_MAX_DELAY`) is honored as a hard minimum: no request is
sent for the scope until it elapses.
#. Otherwise the delay grows exponentially:
.. code-block:: text
delay = min(BACKOFF_MAX_DELAY, max(BACKOFF_MIN_DELAY, delay * BACKOFF_DELAY_FACTOR))
:setting:`BACKOFF_JITTER` is then applied so that requests that backed off
together do not retry in lockstep.
**Recovery** is linear: after a scope goes a full :setting:`BACKOFF_WINDOW`
without a new trigger, its delay drops by one :setting:`BACKOFF_DELAY_FACTOR`
step toward the configured value, and keeps dropping one step per quiet window
until it is back to the configured value. A new trigger resets the countdown.
This exponential-increase / linear-decrease pattern, similar to TCP congestion
control, makes a scope back off quickly when a server is unhappy and return to
full speed gradually once it recovers. To keep a scope hovering around a target
rate instead of repeatedly probing and backing off, enable :ref:`rampup
<rampup>`.
.. _per-scope-backoff:
Per-scope backoff configuration
-------------------------------
The global ``BACKOFF_*`` settings can be overridden per scope with the
``"backoff"`` key of a :setting:`THROTTLING_SCOPES` entry, an instance of
:class:`~scrapy.throttling.BackoffConfig`:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"example.com": {
"backoff": {
"http_codes": [429, 503],
"exceptions": ["builtins.IOError"],
"delay_factor": 1.2,
"max_delay": 180.0,
"min_delay": 5.0,
"jitter": [0.01, 0.33],
},
},
}
Any key left out falls back to the matching global ``BACKOFF_*`` setting.
.. _rampup:
Rampup
======
When using APIs that charge per request, like web scraping APIs, you often want
to maximize throughput while staying within rate limits. To do that, set
``"rampup"`` to ``True`` in :setting:`THROTTLING_SCOPES`:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com": {
"rampup": True,
},
}
Rampup increases concurrency or lowers delay as needed based on the following
setting:
- .. setting:: RAMPUP_BACKOFF_TARGET
:setting:`RAMPUP_BACKOFF_TARGET` (default: ``1``)
Target number of backoff responses per :setting:`BACKOFF_WINDOW` that
:ref:`rampup <rampup>` aims for when probing the rate limit of a scope.
Can be a range like ``[1, 3]``.
For every :setting:`BACKOFF_WINDOW` that stays **below**
:setting:`RAMPUP_BACKOFF_TARGET` backoff triggers, rampup increases throughput
one step: it first lowers the delay, and once the delay reaches its minimum it
raises the concurrency limit above the ``"min_concurrency"`` floor of the
scope. Windows that hit the target hold the current rate, and windows that
exceed it let normal :ref:`backoff <backoff>` reduce the rate. The result is a
rate that converges on roughly :setting:`RAMPUP_BACKOFF_TARGET` rate-limit
responses per window — the most throughput a scope allows without being
penalized.
Rampup behavior can be fine-tuned per scope by giving ``"rampup"`` a dict
instead of ``True``:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com": {
"rampup": {
"backoff_target": [1, 3], # overrides RAMPUP_BACKOFF_TARGET
"delay_factor": 0.5, # multiply the delay by this on each ramp-up step
"min_delay": 0.05, # do not ramp the delay below this
},
},
}
.. _retry-after:
.. _rate-limiting-headers:
Rate limiting headers
=====================
Servers may include `Retry-After
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After>`__
or `RateLimit-Reset
<https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html#name-ratelimit-reset>`__
headers to indicate when you should make your next request. These headers are
respected automatically during :ref:`backoff <backoff>`, using their values as
minimum delays (capped at :setting:`BACKOFF_MAX_DELAY`).
.. seealso:: :setting:`REDIRECT_MAX_DELAY`
.. _crawl-delay:
robots.txt
==========
`Crawl-Delay <https://en.wikipedia.org/wiki/Robots.txt#Crawl-delay_directive>`__
is a non-standard ``robots.txt`` directive that indicates a number of seconds
to wait between requests.
.. setting:: THROTTLING_ROBOTSTXT_OBEY
.. setting:: THROTTLING_ROBOTSTXT_MAX_DELAY
If :setting:`ROBOTSTXT_OBEY` and :setting:`THROTTLING_ROBOTSTXT_OBEY` are
``True`` (default), valid ``Crawl-Delay`` directives override
:setting:`CONCURRENT_REQUESTS_PER_DOMAIN` and :setting:`DOWNLOAD_DELAY`.
Concurrency is set to ``1`` and delay is set to the value of ``Crawl-Delay``, capped at
:setting:`THROTTLING_ROBOTSTXT_MAX_DELAY` (default: ``60.0``).
If :setting:`THROTTLING_SCOPES` defines a different concurrency or delay, it
will be respected, but a warning will be logged about the discrepancy with
``Crawl-Delay``. Set ``ignore_robots_txt`` to ``True`` to silence this warning.
.. _delay-scope:
Delaying a scope programmatically
=================================
You can delay a :ref:`throttling scope <throttling-scopes>` on demand through
:meth:`crawler.throttler.delay_scope()
<scrapy.throttling.ThrottlingManagerProtocol.delay_scope>`:
.. code-block:: python
crawler.throttler.delay_scope("example.com", 30.0)
This holds back every request of the scope for at least the given number of
seconds, counted as a :ref:`backoff <backoff>` trigger.
It is useful to react to situations that :ref:`automatic backoff <backoff>`
cannot detect on its own, such as a soft block that comes back as a ``200``
response. For example, a spider callback can slow down the whole domain when it
detects a maintenance page, and reschedule the current request:
.. code-block:: python
from scrapy import Request, Spider
from scrapy.utils.httpobj import urlparse_cached
class MySpider(Spider):
name = "myspider"
start_urls = ["https://example.com/"]
def parse(self, response):
if "under maintenance" in response.text:
scope = urlparse_cached(response).netloc
self.crawler.throttler.delay_scope(scope, 600.0)
yield response.request.replace(dont_filter=True)
return
# Normal parsing follows.
Unlike :ref:`untrusted delays <rate-limiting-headers>`, this delay is **not**
capped at :setting:`BACKOFF_MAX_DELAY`.
.. _per-request-throttling:
Per-request throttling
======================
Sometimes you need different throttling behavior for individual requests or for
request groups that are not tied to a specific domain.
For example, you might want to throttle API endpoints differently than web
pages on the same domain, group requests by content type (images vs HTML), or
apply different throttling based on request priority.
.. reqmeta:: throttling_scopes
Use the ``throttling_scopes`` request metadata to assign requests to custom
throttling groups:
.. invisible-code-block: python
from scrapy.http import Request
.. code-block:: python
Request("https://api.example/", meta={"throttling_scopes": "api"})
You can also assign multiple throttling groups to a single request:
.. code-block:: python
Request("https://api.example/users", meta={"throttling_scopes": {"api", "users"}})
You can then use the :setting:`THROTTLING_SCOPES` setting to customize
throttling for such requests:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api": {"concurrency": 2},
"users": {"delay": 5.0},
}
.. note:: These custom throttling groups persist through redirects. For
redirect-aware throttling assignment, see :ref:`custom-throttling-scopes`.
.. reqmeta:: throttling_delay
Delaying a single request
-------------------------
To hold a single request for a fixed number of seconds before it is sent,
regardless of its scopes, set the ``throttling_delay`` request metadata key:
.. code-block:: python
Request("https://example.com/slow", meta={"throttling_delay": 5.0})
The delay is applied once, the first time the request reaches the throttling
gate.
``throttling_delay`` defines only the *earliest* time the request may be sent,
not the exact time: once the delay elapses, the request still competes with
every other pending request for its scopes. If you want it sent **as soon as**
its delay elapses, give it a higher :attr:`~scrapy.Request.priority` too:
.. code-block:: python
Request("https://example.com/slow", meta={"throttling_delay": 5.0}, priority=1)
Without a higher priority, a backlog of requests ahead of it in a FIFO queue
could keep it waiting well past the configured delay; a higher priority puts it
at the front of the queue, so it goes out right after its delay.
.. reqmeta:: throttling_dont_track
Excluding a request from throttling state
-----------------------------------------
Some requests (authentication flows, one-off API calls, file downloads) should
not influence throttling state even if they get a :setting:`BACKOFF_HTTP_CODES`
response or raise a :setting:`BACKOFF_EXCEPTIONS` exception. Set the
``throttling_dont_track`` request metadata key to ``True`` to process such a
request normally without letting its outcome trigger :ref:`backoff <backoff>`:
.. code-block:: python
Request("https://example.com/login", meta={"throttling_dont_track": True})
.. _throttling-scopes:
Throttling scopes
=================
Throttling scopes represent aspects of requests that can be throttled
independently.
..
For future reference, the “throttling scope” name was taken from
https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-02.html#section-1.4-4.4
.. _custom-throttling-scopes:
Customizing throttling scopes
------------------------------
There are 2 ways to customize throttling scopes.
To **configure existing scopes**, use the :setting:`THROTTLING_SCOPES` setting.
Its keys are scope names and its values are
:class:`~scrapy.throttling.ThrottlingScopeConfig` dicts, which accept the
following keys:
``concurrency`` (:class:`int`)
Maximum number of concurrent requests for the scope. When unset, the
default depends on the kind of scope: :setting:`CONCURRENT_REQUESTS_PER_DOMAIN`
for domain scopes, :setting:`CONCURRENT_REQUESTS_PER_IP` for IP scopes, and
:setting:`THROTTLING_SCOPE_CONCURRENCY` for any other scope.
``min_concurrency`` (:class:`int`)
Concurrency floor that :ref:`backoff <backoff>` and :ref:`rampup <rampup>`
never drop below. Defaults to ``1``.
``delay`` (:class:`float`)
Minimum seconds between requests for the scope. Defaults to
:setting:`DOWNLOAD_DELAY`.
``jitter`` (:class:`float` or 2-:class:`list`)
Magnitude of the random variation applied to ``delay``; the per-scope
override of :setting:`RANDOMIZE_DOWNLOAD_DELAY`. ``0`` disables it, ``0.5``
means ±50% (the default when :setting:`RANDOMIZE_DOWNLOAD_DELAY` is on), and
a ``[low, high]`` pair multiplies the delay by ``1 + uniform(low, high)``.
``quota`` (:class:`float`)
Maximum :ref:`quota <throttling-quotas>` consumed per ``window``.
``window`` (:class:`float`)
Quota window in seconds. Defaults to :setting:`THROTTLING_WINDOW`.
``rampup`` (:class:`bool` or :class:`dict`)
Enables :ref:`rampup <rampup>` for the scope.
``backoff`` (:class:`~scrapy.throttling.BackoffConfig`)
Per-scope :ref:`backoff overrides <per-scope-backoff>`.
``manager`` (:class:`str` or :class:`type`)
Import path or class of a :ref:`custom scope manager
<custom-throttling-scope-managers>` for the scope.
``ignore_robots_txt`` (:class:`bool`)
Silences the warning logged when this configuration is more aggressive than
a :ref:`robots.txt Crawl-delay <crawl-delay>`.
.. setting:: THROTTLING_MANAGER
To **change how scopes are assigned** (or anything beyond per-scope settings),
set :setting:`THROTTLING_MANAGER` (default:
:class:`~scrapy.throttling.ThrottlingManager`) to a :ref:`component
<topics-components>` that implements the
:class:`~scrapy.throttling.ThrottlingManagerProtocol` protocol (or its import
path as a string):
.. code-block:: python
:caption: ``settings.py``
THROTTLING_MANAGER = "myproject.throttling.MyThrottlingManager"
.. _multiple-throttling-scopes:
Handling of multiple throttling scopes
--------------------------------------
When a request has multiple throttling scopes, it is not sent until all of its
throttling scopes allow it.
.. _throttling-quotas:
Throttling quotas
-----------------
When different requests can consume different amounts of a throttling scope,
you can express this using **throttling quotas**.
.. setting:: THROTTLING_WINDOW
Use the :setting:`THROTTLING_WINDOW` setting (default: ``60.0``) or the ``"window"``
key in the :setting:`THROTTLING_SCOPES` setting to define the time window after
which throttling quotas are reset.
Then use the :setting:`THROTTLING_SCOPES` setting to define the throttling
quotas for each throttling scope:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com": {
"quota": 500.0,
},
}
Then, in the :reqmeta:`throttling_scopes` request metadata key or in the return
value of the :meth:`~scrapy.throttling.ThrottlingManagerProtocol.get_scopes`
method, define a :class:`dict` where keys are throttling scopes and values are
:class:`float` values that indicate the expected quota consumption (it does not
need to be exact).
Everything else being equal, :class:`~scrapy.pqueues.ScrapyPriorityQueue` will
prioritize requests that consume a higher portion of the available throttling
quota, to minimize the risk of those requests getting stuck.
.. _custom-throttling-scope-managers:
Customizing throttling scope managers
-------------------------------------
.. setting:: THROTTLING_SCOPE_MANAGER
The :setting:`THROTTLING_SCOPE_MANAGER` setting (default:
:class:`~scrapy.throttling.ThrottlingScopeManager`) is a :ref:`component
<topics-components>` that implements the
:class:`~scrapy.throttling.ThrottlingScopeManagerProtocol` (or its import path
as a string):
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPE_MANAGER = "myproject.throttling.MyThrottlingScopeManager"
For each throttling scope, an instance of this class is created to manage any
gradual :ref:`backoff <backoff>` or :ref:`rampup <rampup>` required at run
time.
You can implement your own throttling scope manager if you wish to change the
backoff or rampup behavior beyond what settings allow.
You can also define a custom throttling scope manager for a specific throttling
scope by setting the ``"manager"`` key in the :setting:`THROTTLING_SCOPES`
setting:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com": {
"manager": "myproject.throttling.MyThrottlingScopeManager",
},
}
A scope manager only needs to implement the
:class:`~scrapy.throttling.ThrottlingScopeManagerProtocol`. For example, the
following manager enforces a fixed quota per time window without any delay or
exponential backoff, similar to a fixed-window rate limiter:
.. code-block:: python
:caption: ``myproject/throttling.py``
import time
class FixedWindowScopeManager:
def __init__(self, crawler, config):
self.limit = config["quota"]
self.window = config.get("window", 60.0)
self.reset_at = None
self.used = 0.0
self.active = 0
@classmethod
def from_crawler(cls, crawler, config):
return cls(crawler, config)
def can_send(self, now=None, amount=None):
now = now if now is not None else time.monotonic()
if self.reset_at is not None and now >= self.reset_at:
self.reset_at, self.used = None, 0.0
if self.used and self.used + (amount or 0) > self.limit:
return self.reset_at - now
return 0.0
def record_sent(self, now=None, amount=None):
now = now if now is not None else time.monotonic()
if self.reset_at is None:
self.reset_at = now + self.window
self.used += amount or 0
self.active += 1
def record_done(self, now=None):
self.active = max(0, self.active - 1)
def record_backoff(self, delay=None, now=None):
pass # this manager does not back off
def reconcile_quota(self, consumed=None, remaining=None, now=None):
if remaining is not None:
self.used = max(0.0, self.limit - remaining)
elif consumed is not None:
self.used = max(0.0, self.used + consumed)
def set_base_delay(self, delay):
pass
def set_concurrency(self, concurrency):
pass
def is_idle(self, now, max_idle):
return self.active == 0
.. _throttling-aware-scheduler:
Throttling-aware scheduling
===========================
By default, throttling is enforced at the engine, where a request waiting on
its :ref:`throttling scopes <throttling-scopes>` holds a concurrency slot. In a
crawl that mixes heavily-throttled scopes with unthrottled ones, this can let
throttled requests starve unthrottled ones that could be sent right away
(**head-of-line blocking**; Scrapy logs a warning, see
:setting:`DELAYED_REQUESTS_WARN_THRESHOLD`).
:class:`~scrapy.core.scheduler.ThrottlingAwareScheduler` avoids this. To enable
it:
.. code-block:: python
:caption: ``settings.py``
SCHEDULER = "scrapy.core.scheduler.ThrottlingAwareScheduler"
SCHEDULER_PRIORITY_QUEUE = "scrapy.pqueues.ThrottlingAwarePriorityQueue"
.. autoclass:: scrapy.core.scheduler.ThrottlingAwareScheduler
.. _throttling-examples:
Examples
========
.. _alternative-domain-throttling:
Alternative domain throttling
-----------------------------
If you are not happy with the :ref:`default throttling scope behavior
<basic-throttling>` with regards to domains and subdomains, you can change it.
Alternative approaches include:
- Using the **highest-level registrable domain** as the throttling scope,
e.g. https://books.toscrape.com and https://toscrape.com both get a
``toscrape.com`` throttling scope.
This allows to apply the same throttling settings to all subdomains of a
registrable domain.
For example:
.. code-block:: python
:caption: ``settings.py``
import tldextract
from scrapy.utils.httpobj import urlparse_cached
class MyThrottlingManager:
def get_request_scopes(self, request):
extracted = tldextract.extract(request.url)
if extracted.domain and extracted.suffix:
return f"{extracted.domain}.{extracted.suffix}"
return urlparse_cached(request).netloc
THROTTLING_MANAGER = MyThrottlingManager
- Using **multiple throttling scopes per request**, one per registrable
domain and for every higher-level subdomain, e.g.
https://books.toscrape.com and https://toscrape.com both get a
``toscrape.com`` throttling scope, but https://books.toscrape.com also
gets a ``books.toscrape.com`` throttling scope.
This allows to apply the same throttling settings to all subdomains of a
registrable domain, but also allows applying further restrictions on each
or on some subdomains.
For example:
.. code-block:: python
:caption: ``settings.py``
import tldextract
from scrapy.utils.httpobj import urlparse_cached
class MyThrottlingManager:
def get_request_scopes(self, request):
extracted = tldextract.extract(request.url)
if not (extracted.domain and extracted.suffix):
return urlparse_cached(request).netloc
scopes = set()
registrable_domain = f"{extracted.domain}.{extracted.suffix}"
scopes.add(registrable_domain)
if extracted.subdomain:
subdomain_parts = extracted.subdomain.split(".")
for i in range(len(subdomain_parts)):
subdomain = ".".join(subdomain_parts[i:])
full_domain = f"{subdomain}.{registrable_domain}"
scopes.add(full_domain)
return scopes
THROTTLING_MANAGER = MyThrottlingManager
THROTTLING_SCOPES = {
"toscrape.com": {"concurrency": 32},
"books.toscrape.com": {"concurrency": 24},
"quotes.toscrape.com": {"concurrency": 16},
}
Here ``books.toscrape.com`` requests can reach 24 concurrency and
``quotes.toscrape.com`` requests can reach 16 concurrency, but never both
at the same time, because that would sum 40 concurrency, and
``toscrape.com`` requests are limited to 32.
.. _endpoints-throttling:
Endpoint-specific throttling
----------------------------
To apply different throttling settings to different endpoints of the same
domain and not enforce any common throttling, effectively treating them as
different domains:
- Implement a :ref:`throttling manager <custom-throttling-scopes>` that sets
endpoint-specific throttling scopes for that domain:
.. code-block:: python
from scrapy.throttling import ThrottlingManager, scope_cache
from scrapy.utils.httpobj import urlparse_cached
class MyThrottlingManager(ThrottlingManager):
@scope_cache
async def get_scopes(self, request):
parsed_url = urlparse_cached(request)
if parsed_url.netloc != "api.toscrape.com":
return await super().get_scopes(request)
return f"{parsed_url.netloc}{parsed_url.path}"
- Use the :setting:`THROTTLING_SCOPES` setting to set different throttling
settings per endpoint:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com/fast-endpoint": {"concurrency": 1000, "delay": 0.08},
"api.toscrape.com/slow-endpoint": {"delay": 5.0},
}
.. _web-scraping-api-throttling:
Web scraping API throttling
---------------------------
Imagine you are sending requests to a web scraping API, e.g. to avoid bans.
Unless that API provides a Scrapy plugin to make it easier to use, you may want
to:
- Use the :setting:`THROTTLING_SCOPES` setting to increase concurrency for
API requests. For example:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"api.toscrape.com": {"concurrency": 1000, "delay": 0.08},
}
- Implement a :ref:`throttling manager <custom-throttling-scopes>` that:
- Adds a throttling scope for the URL being scraped.
For example, if you request
``https://api.toscrape.com/?url=https://example.com``, by default it
will get a ``api.toscrape.com`` throttling scope, but it should also
get the ``example.com`` throttling scope:
.. code-block:: python
from urllib.parse import urlparse
from scrapy.throttling import add_scope, ThrottlingManager, scope_cache
from scrapy.utils.httpobj import urlparse_cached
from w3lib.url import url_query_parameter
class MyThrottlingManager(ThrottlingManager):
@scope_cache
async def get_scopes(self, request):
scopes = await super().get_scopes(request)
if urlparse_cached(request).netloc != "api.toscrape.com":
return scopes
target_url = url_query_parameter(request.url, "url")
if not target_url:
return scopes
target_domain = urlparse(target_url).netloc
return add_scope(scopes, target_domain)
- Can differentiate between exhaustion of the target website and
exhaustion of the API itself. For example:
.. code-block:: python
from scrapy.throttling import ThrottlingManager
from scrapy.utils.httpobj import urlparse_cached
class MyThrottlingManager(ThrottlingManager):
async def get_response_backoff(self, response):
if (
urlparse_cached(response.request).netloc != "api.toscrape.com"
or response.status != 200
):
return await super().get_response_backoff(response)
upstream_status_code = int(
response.headers.get("X-Upstream-Status-Code", b"200")
)
upstream_response = response.__class__(
response.url,
status=upstream_status_code,
headers=response.headers,
body=response.body,
)
return await super().get_response_backoff(upstream_response)
.. _cost-smoothing-throttling:
Cost-capped throttling
----------------------
Imagine you are using an API that charges different requests differently, e.g.
based on the features used, and you want to limit how much you spend per time
window (:setting:`BACKOFF_WINDOW`). You can use :ref:`throttling quotas
<throttling-quotas>` for that:
- Implement a :ref:`throttling manager <custom-throttling-scopes>` that:
- Sets a ``cost`` throttling scope on each request to some estimation
based e.g. on request URL parameters:
.. code-block:: python
from scrapy.utils.httpobj import urlparse_cached
from scrapy.throttling import ThrottlingManager, scope_cache
class MyThrottlingManager(ThrottlingManager):
@scope_cache
async def get_scopes(self, request):
scopes = await super().get_scopes(request)
parsed_url = urlparse_cached(request)
if parsed_url.netloc != "api.toscrape.com":
return scopes
return add_scope(scopes, "cost", estimate_request_cost(request))
- Reconciles the estimated cost with the actual cost reported by the
response, so that the quota tracks real spending:
.. code-block:: python
from scrapy.throttling import ThrottlingManager, update_scope_backoff
class MyThrottlingManager(ThrottlingManager):
async def get_response_backoff(self, response):
backoff = await super().get_response_backoff(response)
if response.headers.get("X-Actual-Cost") is None:
return backoff
estimated = estimate_request_cost(response.request)
actual = float(response.headers[b"X-Actual-Cost"])
# Report the difference between actual and estimated cost.
return update_scope_backoff(backoff, "cost", consumed=actual - estimated)
- Use the :setting:`THROTTLING_SCOPES` setting to set a maximum cost per time
window:
.. code-block:: python
:caption: ``settings.py``
THROTTLING_SCOPES = {
"cost": {"quota": 100.0},
}
This will allow you to spend up to 100.0 units of cost per time window
(default: 60 seconds) before throttling kicks in.
.. _throttling-per-ip:
Per-IP concurrency limiting
---------------------------
.. setting:: CONCURRENT_REQUESTS_PER_IP
A concurrency limit keyed by IP is a throttling scope whose id is the request's
IP. A request then carries two scopes, its domain and its IP, and is only sent
when **both** allow it (see :ref:`multiple-throttling-scopes`).
Set :setting:`CONCURRENT_REQUESTS_PER_IP` (default: ``0``, disabled) to a
positive number to enable this built in: the throttler adds the IP of each
request, resolved from the DNS cache, as a second scope, limited to that many
concurrent requests. IP scopes whose ``concurrency`` is left unset default to
:setting:`CONCURRENT_REQUESTS_PER_IP`.
For finer control (e.g. resolving IPs that are not in the DNS cache yet, or
grouping several hosts under one address), implement a :ref:`throttling manager
<custom-throttling-scopes>` that adds the request's IP as a second scope
instead:
.. code-block:: python
import socket
from scrapy.throttling import ThrottlingManager, add_scope, scope_cache
from scrapy.utils.asyncio import run_in_thread
from scrapy.utils.httpobj import urlparse_cached
class IPThrottlingManager(ThrottlingManager):
@scope_cache
async def get_scopes(self, request):
scopes = await super().get_scopes(request)
host = urlparse_cached(request).hostname
address = await run_in_thread(socket.gethostbyname, host)
return add_scope(scopes, address)
.. _throttling-settings:
Additional settings
===================
- .. setting:: BACKOFF_EXCEPTIONS
:setting:`BACKOFF_EXCEPTIONS`
Default:
- :exc:`~scrapy.exceptions.DownloadFailedError`
- :exc:`~scrapy.exceptions.DownloadTimeoutError`
- :exc:`~scrapy.exceptions.ResponseDataLossError`
List of exception classes that trigger backoff when raised while
downloading a request. Strings are interpreted as import paths.
.. seealso:: :setting:`RETRY_EXCEPTIONS`
- .. setting:: BACKOFF_JITTER
:setting:`BACKOFF_JITTER` (default: ``0.1``)
Random jitter applied to each backoff delay, as a fraction of the delay.
With the default value of ``0.1`` the delay is randomized by ±10%.
Overrides :setting:`RANDOMIZE_DOWNLOAD_DELAY` during backoff.
- .. setting:: BACKOFF_MIN_DELAY
:setting:`BACKOFF_MIN_DELAY` (default: ``1.0``)
Delay, in seconds, applied on the first backoff step (and the minimum
delay during backoff).
- .. setting:: BACKOFF_WINDOW
:setting:`BACKOFF_WINDOW` (default: ``60.0``)
Time window, in seconds, used by :ref:`backoff <backoff>` and
:ref:`rampup <rampup>`. During backoff, a :ref:`throttling scope
<throttling-scopes>` must go this many seconds without a new backoff
trigger (an HTTP error code from :setting:`BACKOFF_HTTP_CODES` or an
exception from :setting:`BACKOFF_EXCEPTIONS`) before its delay decreases
by one :setting:`BACKOFF_DELAY_FACTOR` step toward the configured value.
A new trigger resets the countdown.
- .. setting:: DELAYED_REQUESTS_WARN_THRESHOLD
:setting:`DELAYED_REQUESTS_WARN_THRESHOLD` (default: ``500``)
Number of requests held back by :ref:`throttling <throttling>` at which
Scrapy logs a warning, to help detect throttling configurations that hold
back more requests than expected.
While throttled, requests in the :ref:`scheduler <topics-scheduler>`
remain in the scheduler. However, requests sent with :meth:`engine.download()
<scrapy.core.engine.ExecutionEngine.download>` bypass the scheduler,
including requests sent by some built-in :ref:`components
<topics-components>` and :ref:`inline requests <inline-requests>`. When
such requests are throttled, they are paused and kept in memory, along
with any run time context from the code that is sending them. If they
accumulate, they can become a memory issue.
- .. setting:: RANDOMIZE_DOWNLOAD_DELAY
:setting:`RANDOMIZE_DOWNLOAD_DELAY` (default: ``True``)
Randomize delays by this factor, e.g. if ``0.2`` randomize delays between
``delay*0.8`` and ``delay*1.2``.
It can be set to a 2-item list with low and high factors, e.g.
``[-0.1, 0.3]`` to randomize delays between ``delay*0.9`` and
``delay*1.3``.
If ``True``, ``0.5`` (i.e. ±50%) is used as the randomization factor. If
``False``, no randomization is applied.
- .. setting:: THROTTLING_DEBUG
:setting:`THROTTLING_DEBUG` (default: ``False``)
Whether to log :ref:`throttling <throttling>` decisions (per-scope delays,
backoff steps and recoveries) for debugging.
- .. setting:: THROTTLING_SCOPE_CONCURRENCY
:setting:`THROTTLING_SCOPE_CONCURRENCY` (default: ``1``)
Default maximum number of concurrent requests for :ref:`throttling scopes
<throttling-scopes>` that are neither domains nor IPs, e.g. custom scopes
added by a :ref:`custom throttling manager <custom-throttling-scopes>`.
Domain and IP scopes use :setting:`CONCURRENT_REQUESTS_PER_DOMAIN` and
:setting:`CONCURRENT_REQUESTS_PER_IP` instead. A scope ``concurrency`` set
in :setting:`THROTTLING_SCOPES` overrides this.
- .. setting:: THROTTLING_SCOPE_LIMIT
:setting:`THROTTLING_SCOPE_LIMIT` (default: ``100000``)
Maximum number of :ref:`throttling scope <throttling-scopes>` states kept
in memory at once, to bound memory usage on broad crawls that touch a large
number of scopes (e.g. domains).
When the limit is exceeded, the least-recently-used idle scopes are evicted
(an evicted scope is recreated from its configuration the next time it is
needed). Scopes with in-flight requests or in active backoff are never
evicted, so the limit may be temporarily exceeded if that many scopes are
busy at once. Set to ``0`` to disable the limit.
This complements :setting:`THROTTLING_SCOPE_MAX_IDLE`, which evicts scopes
by inactivity time rather than by count.
- .. setting:: THROTTLING_SCOPE_MAX_IDLE
:setting:`THROTTLING_SCOPE_MAX_IDLE` (default: ``3600.0``)
Seconds of inactivity after which the state of a :ref:`throttling scope
<throttling-scopes>` is evicted from memory to bound memory usage on
long-running crawls. Set to ``0`` to never evict. Scopes in active backoff
are never evicted.
.. _autothrottle-migration:
Migrating from AutoThrottle
===========================
The ``AutoThrottle`` extension is deprecated in favor of the throttling and
:ref:`backoff <backoff>` system described here, which is always active and does
not need to be enabled.
Setting ``AUTOTHROTTLE_ENABLED`` to ``True`` still works but logs a
deprecation warning. To migrate, drop the ``AUTOTHROTTLE_*`` settings and use
the following equivalents:
.. list-table::
:header-rows: 1
* - AutoThrottle
- Throttling
* - ``AUTOTHROTTLE_ENABLED = True``
- No equivalent; throttling is always active.
* - ``AUTOTHROTTLE_START_DELAY``
- :setting:`DOWNLOAD_DELAY`
* - ``AUTOTHROTTLE_MAX_DELAY``
- :setting:`BACKOFF_MAX_DELAY`
* - ``AUTOTHROTTLE_TARGET_CONCURRENCY``
- :ref:`rampup <rampup>` (``"rampup": True``)
* - ``AUTOTHROTTLE_DEBUG = True``
- :setting:`THROTTLING_DEBUG` ``= True``
* - ``autothrottle_dont_adjust_delay`` (request meta)
- :reqmeta:`throttling_dont_track` (request meta)
AutoThrottle adjusted the delay of each download slot based on response
latency. The new system does not measure latency; instead, it reacts to
explicit rate-limit signals (:setting:`BACKOFF_HTTP_CODES`,
:setting:`BACKOFF_EXCEPTIONS`, :ref:`Retry-After / RateLimit-Reset
<rate-limiting-headers>`) and, with :ref:`rampup <rampup>`, probes for the
fastest rate a scope tolerates. If you specifically need latency-based control,
implement a custom :ref:`throttling scope manager
<custom-throttling-scope-managers>`.
.. _throttling-api:
API
===
.. autoclass:: scrapy.throttling.ThrottlingManagerProtocol
:members:
:member-order: bysource
.. autoclass:: scrapy.throttling.ThrottlingManager
:members: get_response_delay
.. autoclass:: scrapy.throttling.ThrottlingScopeManagerProtocol
:members:
:member-order: bysource
.. autoclass:: scrapy.throttling.ThrottlingScopeManager
.. autoclass:: scrapy.pqueues.ThrottlingAwarePriorityQueue
.. autoclass:: scrapy.throttling.ThrottlingScopeConfig
.. autoclass:: scrapy.throttling.BackoffConfig
.. autofunction:: scrapy.throttling.scope_cache
.. autofunction:: scrapy.throttling.add_scope
.. autofunction:: scrapy.throttling.update_scope_backoff