sparckles / Robyn

Robyn is a Super Fast Async Python Web Framework with a Rust runtime.
https://robyn.tech/
BSD 2-Clause "Simplified" License
4.25k stars 218 forks source link

Add rate limit throttling option #524

Closed IdoKendo closed 1 year ago

IdoKendo commented 1 year ago

This issue proposes to add an optional way to include rate limiting on endpoints. Example usage:

@app.get("/", rate_limit=(3, 1))
async def hello_world():
    return "Hello world"

In this case, the limit will be 3 requests per minute:

❯ curl http://127.0.0.1:8080/ -s -o /dev/null -w "%{http_code}"
200
❯ curl http://127.0.0.1:8080/ -s -o /dev/null -w "%{http_code}"
200
❯ curl http://127.0.0.1:8080/ -s -o /dev/null -w "%{http_code}"
200
❯ curl http://127.0.0.1:8080/ -s -o /dev/null -w "%{http_code}"
429

In order to allow multiple workers to track the same rate limit, there will be a need to use a store such as Redis to keep the information. The usage information should be per endpoint and client.

The Redis client can be stored either as an environment variable (ROBYN_REDIS) or as a startup argument (--redis).

In case there is no Redis client defined and any endpoint has a stated rate_limit, the app would store the information in memory. This should be noted on startup in case any endpoint in the app defines a rate limit.

sansyrox commented 1 year ago

Hey Ido 👋

I was thinking of adding a rate limiting option too!

I have also prepared an interface that I was expecting, which I will upload later today.

We can discuss more after that??

IdoKendo commented 1 year ago

Sure thing! Looking forward to it.

sansyrox commented 1 year ago

@IdoKendo , I finally found it in my notes.

I just wanted to add the following. Create a Rate limiting function that would work well on the routers.

from robyn import RateLimiter

limiter = RateLimiter(app, "100/minute;10/second")
IdoKendo commented 1 year ago

Few questions:

  1. Would you then use this limiter as part of the decorator? i. e.
    
    from robyn import RateLimiter
    from robyn import Robyn

app = Robyn(file) limiter = RateLimiter(app, "100/minute;10/second")

@app.get("/", limiter=limiter) async def hello_world(): return "Hello world"

Or did you have some other implementation in mind? In my opinion, this is nicer, since you can have different limits on different routes. Maybe some routes need stricter limits than others etc.

2. On the other hand, do you think that this should also allow for global limiting, e.g:
```py
from robyn import RateLimiter
from robyn import Robyn

app = Robyn(__file__)
limiter = RateLimiter(app, "100/minute;10/second")
app.set_limit(limiter)

@app.get("/")
async def hello_world():
    return "Hello world"
  1. Lastly, on a different note, I started looking into the implementation and I don't see that we keep any client request data to be able to track limits per client, does it exist somewhere and I just missed it, or should I work on implementing it first?
IdoKendo commented 1 year ago

After much deliberation and back-and-forth, I've decided to make this feature an external plugin. The repo is located at https://github.com/IdoKendo/robyn_rate_limits and it's publish to PyPI under robyn-rate-limits.