Ratelimit in Fastapi
Asked Answered
F

5

21

How to ratelimit API endpoint request in Fastapi application ? I need to ratelimit API call 5 request per second per user and exceeding that limit blocks that particular user for 60 seconds.

In main.py

def get_application() -> FastAPI:
     application = FastAPI(title=PROJECT_NAME, debug=DEBUG, version=VERSION)
     application.add_event_handler(
        "startup", create_start_app_handler(application))
     application.add_event_handler(
        "shutdown", create_stop_app_handler(application))
     return application
app = get_application()

In events.py

def create_start_app_handler(app: FastAPI) -> Callable:  
    async def start_app() -> None:           

        redis = await aioredis.create_redis_pool("redis://localhost:8080")
        FastAPILimiter.init(redis)
    return start_app

In endpoint

@router.post('/user',
             tags=["user"],
             name="user:user", dependencies=[Depends(RateLimiter(times=5, seconds=60))])
***code****

Run from this file test.py.

import uvicorn

from app.main import app

if __name__ == "__main__":
    uvicorn.run("test:app", host="0.0.0.0", port=8000, reload=True)

I edited as above but got following error.

File "****ite-packages\starlette\routing.py", line 526, in lifespan
    async for item in self.lifespan_context(app):
  File "****site-packages\starlette\routing.py", line 467, in default_lifespan
    await self.startup()
  File "****site-packages\starlette\routing.py", line 502, in startup
    await handler()
  File "****app\core\services\events.py", line 15, in start_app
    redis = await aioredis.create_redis_pool("redis://localhost:8080")
  File "****\site-packages\aioredis\commands\__init__.py", line 188, in create_redis_pool
    pool = await create_pool(address, db=db,
  File "****site-packages\aioredis\pool.py", line 58, in create_pool
    await pool._fill_free(override_min=False)
  File "C****\site-packages\aioredis\pool.py", line 383, in _fill_free
    conn = await self._create_new_connection(self._address)
  File "****site-packages\aioredis\connection.py", line 111, in create_connection
    reader, writer = await asyncio.wait_for(open_connection(
  File "****\asyncio\tasks.py", line 455, in wait_for
    return await fut
  File "****\site-packages\aioredis\stream.py", line 23, in open_connection
    transport, _ = await get_event_loop().create_connection(
  File "****\asyncio\base_events.py", line 1033, in create_connection
    raise OSError('Multiple exceptions: {}'.format(
OSError: Multiple exceptions: [Errno 10061] Connect call failed ('::1', 8080, 0, 0), [Errno 10061] Connect call failed ('127.0.0.1', 8080)
Feleciafeledy answered 29/12, 2020 at 11:9 Comment(3)
Is there any rate limiter implementation for FastApi and websocket protocol? I am investing do it one for my needs.Rauscher
I'd suggest to run your app behind a full-fledged web browser like nginx which provides great rate limiting functionality.Mcelroy
@FrancoGil - "websocket endpoints are not supported yet" (source)Stupefy
W
45

Best option is using a library since FastAPI does not provide this functionality out-of-box.

slowapi is great, and easy to use.

You can use ut like this.

from fastapi import FastAPI
from slowapi.errors import RateLimitExceeded
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address


limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/home")
@limiter.limit("5/minute")
async def homepage(request: Request):
    return PlainTextResponse("test")

@app.get("/mars")
@limiter.limit("5/minute")
async def homepage(request: Request, response: Response):
    return {"key": "value"}
Weeks answered 29/12, 2020 at 11:31 Comment(2)
I couldn't edit it myself to add the import of from slowapi.errors import RateLimitExceeded, referencing the docs: slowapi.readthedocs.io/en/latestAstrograph
Also note that Request must be a Starlette request. Doesn't seem like you can use Pydantic types?Acolyte
S
11

FastAPI doesn't natively support this, but it's possible with a few libraries such the ones below, but will usually require some sort of database backing(redis, memcached, etc), although slowapi has a memory fallback in case of no database.

In order to use fastapi-limiter, as seen in their documentation:

Note: You will need a running Redis for this to work.

import aioredis
import uvicorn
from fastapi import Depends, FastAPI

from fastapi_limiter import FastAPILimiter
from fastapi_limiter.depends import RateLimiter

app = FastAPI()


@app.on_event("startup")
async def startup():
    redis = await aioredis.create_redis_pool("redis://localhost")
    FastAPILimiter.init(redis)


@app.get("/", dependencies=[Depends(RateLimiter(times=2, seconds=5))])
async def index():
    return {"msg": "Hello World"}


if __name__ == "__main__":
    uvicorn.run("main:app", debug=True, reload=True)
Stripteaser answered 29/12, 2020 at 11:31 Comment(2)
Got error .Added on question after adding fastapi-limiterFeleciafeledy
File "****\site-packages\aioredis\stream.py", line 23, in open_connection this would probably mean that the redis server isn't up and running, which fastapi-limiter requires.Stripteaser
S
7

You can use https://github.com/abersheeran/asgi-ratelimit

Compared to https://pypi.org/project/fastapi-limiter/ and https://pypi.org/project/slowapi/, it can better meet your needs.

This is a example: after exceeding the five times per second access limit, block a specific user for 60 seconds.

app.add_middleware(
    RateLimitMiddleware,
    authenticate=AUTH_FUNCTION,
    backend=RedisBackend(),
    config={
        r"^/user": [Rule(second=5, block_time=60)],
    },
)
Scintillation answered 31/12, 2021 at 9:19 Comment(3)
How to write auth_fuction for blocking IP address or registered user's id ?Feleciafeledy
Does this ratelimiter works for app working in AWS Lambda?Feleciafeledy
@HimalAcharya You can use it in any ASGI applicatoin. About write auth_function, please read the project README, it's a bit long.Scintillation
M
4

Rather than using external packages which use a database to store data that is needed for only a few seconds or minutes, I prefer to use this:

Here in class RateLimiter requests_limit total limit of request use can perform in time_window seconds.

Remember: I am using client IP to track their request behavior and put a limit if requests_limit exceed within time_window seconds.

Note: It is a dependency so it may not work with middleware of fast API (I haven't tested it)

from fastapi import FastAPI, Request, HTTPException, Depends
import time

# Initialize FastAPI app
app = FastAPI()

# In-memory storage for request counters
request_counters = {}

# Custom RateLimiter class with dynamic rate limiting values per route
class RateLimiter:
    def __init__(self, requests_limit: int, time_window: int):
        self.requests_limit = requests_limit
        self.time_window = time_window

    async def __call__(self, request: Request):
        client_ip = request.client.host
        route_path = request.url.path

        # Get the current timestamp
        current_time = int(time.time())

        # Create a unique key based on client IP and route path
        key = f"{client_ip}:{route_path}"

        # Check if client's request counter exists
        if key not in request_counters:
            request_counters[key] = {"timestamp": current_time, "count": 1}
        else:
            # Check if the time window has elapsed, reset the counter if needed
            if current_time - request_counters[key]["timestamp"] > self.time_window:
                # Reset the counter and update the timestamp
                request_counters[key]["timestamp"] = current_time
                request_counters[key]["count"] = 1
            else:
                # Check if the client has exceeded the request limit
                if request_counters[key]["count"] >= self.requests_limit:
                    raise HTTPException(status_code=429, detail="Too Many Requests")
                else:
                    request_counters[key]["count"] += 1

        # Clean up expired client data (optional)
        for k in list(request_counters.keys()):
            if current_time - request_counters[k]["timestamp"] > self.time_window:
                request_counters.pop(k)

        return True

# Include the custom RateLimiter dependency on specific routes
@app.get("/limited", dependencies=[Depends(RateLimiter(requests_limit=10, time_window=60))])
async def limited_endpoint():
    return {"message": "This endpoint has rate limiting (10 requests per 60 seconds)."}

@app.get("/limited/other", dependencies=[Depends(RateLimiter(requests_limit=5, time_window=60))])
async def limited_other_endpoint():
    return {"message": "This endpoint has rate limiting (5 requests per 60 seconds)."}

@app.get("/unlimited")
async def unlimited_endpoint():
    return {"message": "This endpoint has no rate limiting."}

we create a custom RateLimiter class with the __init__ method to accept custom rate limiting values (requests_limit and time_window) as parameters. The class implements the __call__ method, which allows the class instance to be used as a dependency callable.

Maurine answered 6/8, 2023 at 13:8 Comment(0)
S
1

fastapi-limiter and slowapi is very beautiful package to implement Ratelimit in Fastapi.

but use walrus can also do it. but should start redis database.

  1. start a redis.

  2. python code: write a python file: code1228.py

  3. code:

from walrus import Database, RateLimitException
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uvicorn

db = Database()
rate = db.rate_limit('xxx', limit=5, per=60)  # in 60s just can only click 5 times

app = FastAPI()


@app.exception_handler(RateLimitException)
def parse_rate_litmit_exception(request: Request, exc: RateLimitException):
    msg = {'success': False, 'msg': f'please have a tea for sleep, your ip is: {request.client.host}.'}
    return JSONResponse(status_code=429, content=msg)


@app.get('/')
def index():
    return {'success': True}


@app.get('/important_api')
@rate.rate_limited(lambda request: request.client.host)
def query_important_data(request: Request):
    data = 'important data'
    return {'success': True, 'data': data}


if __name__ == "__main__":
    uvicorn.run("code1228:app", debug=True, reload=True)

  1. run this python file.

  2. test the link.http://127.0.0.1:8000/important_api

Shields answered 28/12, 2021 at 14:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.