Throttling
==========

``django-modern-rest`` ships its own throttling
(also known as "rate limiting") mechanism.
Here's how everything works.

.. important::

  If you have an option not to use ratelimiting in Django,
  but to use it on the HTTP Proxy side, you should prefer the proxy.
  It is significantly faster and more secure.


Defining throttling
-------------------

We have two classes to define throttling:

- :class:`dmr.throttling.SyncThrottle` for sync endpoints
- :class:`dmr.throttling.AsyncThrottle` for async endpoints

We can define throttling on three different levels:

.. tabs::

  .. tab:: per endpoint

    .. literalinclude:: /examples/throttling/per_endpoint.py
      :caption: views.py
      :linenos:
      :language: python

  .. tab:: per controller

    .. literalinclude:: /examples/throttling/per_controller.py
      :caption: views.py
      :linenos:
      :language: python

  .. tab:: per settings

    Set :data:`~dmr.settings.Settings.throttling` setting
    to enable throttling for all controllers.

    .. code-block:: python
      :caption: settings.py
      :linenos:

      >>> from dmr.settings import Settings
      >>> from dmr.throttling import SyncThrottle, Rate

      >>> DMR_SETTINGS = {Settings.throttling: [SyncThrottle(5, Rate.minute)]}

    When your project mixes sync and async endpoints,
    use :class:`~dmr.throttling.SyncOrAsyncThrottle`
    in settings:

    .. code-block:: python
      :caption: settings.py

      >>> from dmr.settings import Settings
      >>> from dmr.throttling import (
      ...     AsyncThrottle,
      ...     Rate,
      ...     SyncOrAsyncThrottle,
      ...     SyncThrottle,
      ... )

      >>> DMR_SETTINGS = {
      ...     Settings.throttling: [
      ...         SyncOrAsyncThrottle(
      ...             SyncThrottle(5, Rate.minute),
      ...             AsyncThrottle(5, Rate.minute),
      ...         ),
      ...     ],
      ... }

    .. note::

      :class:`~dmr.throttling.SyncOrAsyncThrottle` is only allowed
      in ``DMR_SETTINGS``. Using it on a controller or endpoint
      raises :exc:`~dmr.exceptions.EndpointMetadataError`.

Providing several throttling instances means that all of them must succeed.
When multiple throttling rules are defined
on different levels, their rules are joined.

For example:

.. literalinclude:: /examples/throttling/multiple.py
  :caption: views.py
  :linenos:
  :language: python

Will guard ``GET`` method with 2 throttling checks:

1. Not more ``<=`` than 1 request per minute
2. And not more ``<=`` than 5 requests per hour


Customizing throttling
----------------------

Rates
~~~~~

:class:`~dmr.throttling.Rate` is passed as the second required
parameter to throttle classes. However, all values that you pass
are just numbers of seconds. So, you can fully customize throttling
timings by passing any amount of seconds that you wish:

.. code-block:: python
  :caption: settings.py
  :linenos:

  >>> from dmr.settings import Settings, DMR_SETTINGS
  >>> from dmr.throttling import SyncThrottle

  >>> DMR_SETTINGS = {
  ...     Settings.throttling: [
  ...          SyncThrottle(
  ...              max_requests=5,
  ...              duration_in_seconds=10,
  ...          ),
  ...     ],
  ... }

This will set a throttling rule: no more than 5 requests in 10 seconds.

Backends
~~~~~~~~

Backends are used to define where we store throttling data.

By default we use:

- :class:`dmr.throttling.backends.SyncDjangoCache` for sync endpoints
- :class:`dmr.throttling.backends.AsyncDjangoCache` for async endpoints

All backends that we support can be further customized.

.. tabs::

  .. tab:: DjangoCache

    By default we store all the data in the ``'default'`` Django cache.
    You can customize which Django cache name is used. For example:

    .. literalinclude:: /examples/throttling/cache_customization.py
      :caption: views.py
      :linenos:
      :language: python

  .. tab:: Redis

    Any Redis-compliant tool is supported, including: Valkey, KeyDB, etc.

    You can fully customize the client:

    .. literalinclude:: /examples/throttling/redis_backend.py
      :caption: views.py
      :linenos:
      :language: python

    .. note::

      Make sure that `redis <https://pypi.org/project/redis/>`_ client
      library is installed, we don't ship
      it together with ``django-modern-rest``.

You can also write your own backends, for example,
to store throttling information in memory, filesystem, or somewhere else.
To do so, you would need to subclass
:class:`dmr.throttling.backends.BaseThrottleSyncBackend`
or :class:`dmr.throttling.backends.BaseThrottleAsyncBackend`
and override 2 methods.

Full list of backends that we ship in ``django-modern-rest``:

- :class:`~dmr.throttling.backends.SyncDjangoCache`
  and :class:`~dmr.throttling.backends.AsyncDjangoCache`, default
- :class:`~dmr.throttling.backends.redis.SyncRedis`
  and :class:`~dmr.throttling.backends.redis.AsyncRedis`

.. warning::

  When using :class:`~dmr.throttling.backends.SyncDjangoCache`
  or :class:`~dmr.throttling.backends.AsyncDjangoCache`
  the final behavior will depend on the cache that you use.

  Some Django cache backends like
  ``django.core.cache.backends.locmem.LocMemCache``
  store cache in memory per-process. So, any multiprocess environments
  with ``N`` processes will allow to use ``N * max_request`` requests.
  Using such cache backends is not safe.

  Some like ``django.core.cache.backends.dummy.DummyCache`` do nothing at all.

Choosing a backend
^^^^^^^^^^^^^^^^^^

.. list-table::
   :header-rows: 1
   :widths: 22 18 12 24 24

   * - Backend
     - Atomicity
     - Overhead
     - Supported algorithms
     - Best suited for
   * - ``DjangoCache``
     - Per-process: multiprocess deployments may face problems
     - Very low (depends on the cache type)
     - All
     - Non-critical IP based checks with not-strict windows and limits
   * - ``Redis``
     - Full
     - Low
     - All builtin ones, but requires ``lua`` scripting support
     - Strict distributed limits

Unsafe backend warning
^^^^^^^^^^^^^^^^^^^^^^

By default, ``django-modern-rest`` emits
a :class:`~dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning`
warning when detecting an unsafe cache backend for throttling.

You can configure this check on three levels as usual:

1. Per endpoint: pass ``throttling_allow_unsafe_cache`` parameter
2. Per controller: by setting ``throttling_allow_unsafe_cache`` attribute
3. In settings, see :data:`~dmr.settings.Settings.throttling_allow_unsafe_cache`

When ``throttling_allow_unsafe_cache`` is set to ``False``,
we raise a :exc:`dmr.exceptions.EndpointMetadataError`
exception instead of a warning. This setting will ensure the maximum safety.

To suppress this check completely and run throttling at your own risk,
set :data:`~dmr.settings.Settings.throttling_allow_unsafe_cache` to ``None``.


Algorithms
~~~~~~~~~~

Algorithms are used to define the logic of how requests are counted.

By default we use :class:`dmr.throttling.algorithms.SimpleRate`
as the algorithm.
It defines a fixed window with a fixed amount of requests possible.
When window is expired, it resets the count of requests.

Here's how you can customize the algorithm for a throttling:

.. literalinclude:: /examples/throttling/algorithm_customization.py
  :caption: views.py
  :linenos:
  :language: python

You can also write your own algorithms.
To do so, you would need to subclass
:class:`dmr.throttling.algorithms.BaseThrottleAlgorithm`
and override 2 required methods.

Full list of algorithms that we ship in ``django-modern-rest``:

- :class:`~dmr.throttling.algorithms.SimpleRate`, default
- :class:`~dmr.throttling.algorithms.LeakyBucket` where requests fill
  the bucket; tokens leak at a steady rate. Unlike ``SimpleRate``,
  drains continuously providing smoother rate-limiting
  without allowing bursts at window boundaries.

.. warning::

  :class:`~dmr.throttling.algorithms.SimpleRate` uses a **fixed window**
  that resets when the window expires. This allows a boundary burst pattern:
  a client can send ``N`` requests right before the window resets and ``N``
  more right after, effectively firing ``2N`` requests in a short interval
  while remaining within the configured per-window limit.

  For abuse-sensitive endpoints (login, OTP, password reset) prefer
  :class:`~dmr.throttling.algorithms.LeakyBucket`, which drains
  continuously and eliminates this burst window.

Choosing an algorithm
^^^^^^^^^^^^^^^^^^^^^

.. list-table::
   :header-rows: 1
   :widths: 22 18 12 24 24

   * - Algorithm
     - Window type
     - Overhead
     - Boundary burst risk
     - Best suited for
   * - :class:`~dmr.throttling.algorithms.SimpleRate`
     - Fixed — resets after ``duration``
     - Very low
     - Yes — up to ``2N`` requests in a short burst
     - General-purpose, internal, and admin endpoints
   * - :class:`~dmr.throttling.algorithms.LeakyBucket`
     - Continuous drain
     - Low
     - No — traffic is smoothed regardless of timing
     - Auth endpoints (login, OTP, password reset), public APIs

For auth and abuse-sensitive endpoints, use
:class:`~dmr.throttling.algorithms.LeakyBucket`:

.. literalinclude:: /examples/throttling/auth_leaky_bucket.py
  :caption: views.py
  :linenos:
  :language: python

Cache keys
~~~~~~~~~~

Cache keys is what defines how requests are identified.

By default we use :func:`dmr.throttling.cache_keys.RemoteAddr` cache key,
which identifies requests by IP taken from
`REMOTE_ADDR <https://docs.djangoproject.com/en/stable/ref/request-response/#django.http.HttpRequest.META>`_
value from ``request.META``.

.. warning::

  If you are using reverse proxies, make sure to correctly configure
  how they pass request headers, to ``REMOTE_ADDR`` would be correct.

You can write your own cache keys, they are subclasses
of :class:`~dmr.throttling.cache_keys.BaseThrottleCacheKey`
and must return a string or ``None``.

If cache key returns ``None``, it means that this request
will be skipped from this exact throttling check.
However, other keys may still be applied.

It is useful to skip some requests from throttling checks,
for example, from paid or stuff users.

Full list of cache keys that we ship in ``django-modern-rest``:

- :class:`~dmr.throttling.cache_keys.RemoteAddr`, default
- :class:`~dmr.throttling.cache_keys.UserPk`, based on ``request.user``,
  by default we use ``request.user.pk`` if it exists.
  You can pass ``exclude_stuff`` argument as ``False``
  to also limit ``is_stuff`` users,
  or you can pass ``exclude_superuser`` argument as ``False``
  to also limit super users
- :class:`~dmr.throttling.cache_keys.JwtToken`, based on
  ``request.__dmr_jwt__``.
  Uses ``jti`` claim when present and falls back to ``sub`` claim.
  Raw value is hashed before being used as a cache key.
  Returns ``None`` when ``request.__dmr_jwt__`` is not set.

When throttling is executed
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Throttling is executed in two stages: before auth and after auth.
Why? Because we need to:

1. Protect auth from abusive requests and brute forcing
2. Make sure we can base throttling rules on the auth info

The same can be said about content negotiation,
it also must be protected by throttling.
Otherwise, people can abuse content negotiation without any request limits.

.. mermaid::
  :caption: Throttling execution
  :config: {"theme": "forest"}

  graph
      Start[New request] --> BeforeThrottle[Throttling based on IP or 429];
      BeforeThrottle --> RendererNegotiation[Renderer is negotiated or 406];
      RendererNegotiation --> Auth[Auth or 401];
      Auth --> AfterThrottle[Throttling based on auth or 429];

All cache keys know when to execute by default, however you can customize this.
For example, you can run some IP based throttling checks after the auth itself:

.. literalinclude:: /examples/throttling/throttling_after_auth.py
  :caption: views.py
  :linenos:
  :language: python

.. warning::

  It is **strongly** not recommended to have
  auth without any throttling before it.

  Auth must be protected from brute force and denial of service attacks!
  For example, one can also use
  `django-axes <https://github.com/jazzband/django-axes>`_ for this.

  `wemake-django-template <https://github.com/wemake-services/wemake-django-template>`_
  has this configured properly.

Note that it won't make any sense to run auth-based throttling before auth.
So, customize it with care.


Headers
~~~~~~~

By default on
`429 Too Many Requests <https://developer.mozilla.org/de/docs/Web/HTTP/Reference/Status/429>`_
error we return four headers:

- ``X-RateLimit-Limit`` - The maximum number of requests permitted
  in the current time window
- ``X-RateLimit-Remaining`` - The number of requests remaining
  in the current time window
- ``X-RateLimit-Reset`` - The number of seconds until the current
  rate limit window resets
- ``Retry-After`` - The number of seconds until the current
  rate limit window resets, see
  `RFC-6585 <https://datatracker.ietf.org/doc/html/rfc6585#section-4>`_
  and `RFC-7231 <https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.3>`_

.. note::

  Headers with ``X-`` prefix means that they are custom ones,
  there's no spec behind them.
  However, this convention is the most popular one as of right now.

OpenAPI support is built in for this feature.
All headers classes will provide the proper
:class:`~dmr.headers.HeaderSpec` for the ``429`` response.

You might want to customize the returned headers. To do so,
you can pass ``response_headers`` argument to throttling classes
with header classes that you actually want to support.

For example, you can disable
`Retry-After <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After>`_
header with:

.. literalinclude:: /examples/throttling/header_disable_retry_after.py
  :caption: views.py
  :linenos:
  :language: python

Or if you want to support
the `latest draft <https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-10.html>`_
about ``RateLimit`` and ``RateLimit-Policy`` headers, you can use:

.. literalinclude:: /examples/throttling/header_ietf_draft.py
  :caption: views.py
  :linenos:
  :language: python

You can also combine these headers with each other in any combinations.
You can write your own classes with custom headers support.
To do so, subclass :class:`dmr.throttling.headers.BaseResponseHeadersProvider`.

You can completely disable any extra response headers by passing an empty list.

Full list of header providers that we ship in ``django-modern-rest``:

- :class:`~dmr.throttling.headers.XRateLimit`, default
- :class:`~dmr.throttling.headers.RetryAfter`, default
- :class:`~dmr.throttling.headers.RateLimitIETFDraft`


Security
--------

Key considerations:

- Be sure to correctly setup your HTTP Proxy server to send correct IP headers
- Be especially careful with ``X-Forwarded-For`` header,
  because it can contain several layers of proxies
- Never rate limit on user supplied data such as ``User-Agent``,
  because this data can easily be changed
- Denial of service: be careful not to limit other users when limiting just one
- Do not store sensitive or personal users' data in your cache keys,
  because it is stored with no protection / encryption

.. seealso::

  https://django-ratelimit.readthedocs.io/en/latest/security.html


Throttling reports
------------------

If you need to attach any throttling headers to successful responses,
you can do it as well.

For this we offer two APIs:

- :meth:`dmr.throttling.ThrottlingReport.report` for sync APIs
- :meth:`dmr.throttling.ThrottlingReport.areport` for async APIs

All our regular rules apply:

- All new headers must be added to the corresponding
  :class:`~dmr.metadata.ResponseSpec` definitions
- When settings headers, you would need to use :func:`~dmr.endpoint.validate`

.. literalinclude:: /examples/throttling/reports.py
  :caption: views.py
  :linenos:
  :language: python

Use ``headers`` argument to :meth:`~dmr.controller.Controller.to_response`
to add needed headers.

.. warning::

  :class:`~dmr.throttling.ThrottlingReport` will make ``N`` cache requests
  when building header reports (where ``N`` is the number of throttle
  instances used for this endpoint).

  It might be slow, depending on the number of throttles and your cache.

  It might also fail, we don't handle any errors
  in the reports building process.

  Use this feature only when there's a serious need for it.

You can also provide the same headers not just for successful responses,
but for any errors that consume ratelimit quota as well.

To so, you would need
to :ref:`customize headers spec <customizing-error-messages>`
of your error model:

.. literalinclude:: /examples/throttling/reports_for_errors.py
  :caption: views.py
  :linenos:
  :language: python

Method ``to_response`` is used for both successful and error responses.
This way both your successful responses
and error responses will have the needed ratelimiting headers.


Testing
-------

.. note::

  ``dmr.test`` and our ``pytest`` plugin ship helpers to drive an endpoint
  to its limit and assert the ``429`` -- ``assert_throttling``,
  ``reduced_throttling`` and ``assert_throttled``.
  See :ref:`testing-throttling` for details.


API Reference
-------------

Base
~~~~

.. autoclass:: dmr.throttling.SyncThrottle
  :members:
  :inherited-members:

.. autoclass:: dmr.throttling.AsyncThrottle
  :members:
  :inherited-members:

.. autoclass:: dmr.throttling.SyncOrAsyncThrottle
  :members:

.. autoclass:: dmr.throttling.Rate
  :members:

.. autoclass:: dmr.throttling.ThrottlingReport
  :members:

Backends
~~~~~~~~

.. autoclass:: dmr.throttling.backends.CachedRateLimit
  :members:
  :show-inheritance:

.. autoclass:: dmr.throttling.backends.BaseThrottleSyncBackend
  :members:

.. autoclass:: dmr.throttling.backends.BaseThrottleAsyncBackend
  :members:

.. autoclass:: dmr.throttling.backends.SyncDjangoCache
  :members:

.. autoclass:: dmr.throttling.backends.AsyncDjangoCache
  :members:

.. autoclass:: dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning
  :members:

.. autoclass:: dmr.throttling.backends.redis.SyncRedis
  :members:

.. autoclass:: dmr.throttling.backends.redis.AsyncRedis
  :members:

Algorithms
~~~~~~~~~~

.. autoclass:: dmr.throttling.algorithms.BaseThrottleAlgorithm
  :members:

.. autoclass:: dmr.throttling.algorithms.SimpleRate
  :members:

.. autoclass:: dmr.throttling.algorithms.LeakyBucket
  :members:

Cache keys
~~~~~~~~~~

.. autoclass:: dmr.throttling.cache_keys.BaseThrottleCacheKey
  :members:

.. autoclass:: dmr.throttling.cache_keys.RemoteAddr
  :members:

.. autoclass:: dmr.throttling.cache_keys.UserPk
  :members:

.. autoclass:: dmr.throttling.cache_keys.JwtToken
  :members:

Headers
~~~~~~~

.. autoclass:: dmr.throttling.headers.BaseResponseHeadersProvider
  :members:

.. autoclass:: dmr.throttling.headers.XRateLimit
  :members:

.. autoclass:: dmr.throttling.headers.RetryAfter
  :members:

.. autoclass:: dmr.throttling.headers.RateLimitIETFDraft
  :members:
