Package: pyredlight
PyRedLightRate limiting for Python with fast Redis transactions.Simply put, instead of running multiple individual operations leading to multiple round-trips to your Redis server, we
execute a small transaction consisting of the following commands (for a limit of60/10s)SET key 60 EX 10 NX   # Create key and set expiration time, if it does not exist
DECR key              # Decrement value and return
TTL key               # Return time until key expiresThen from the results we parse how many requests you're still allowed to perform within the limit (if any), and what is
the expiration time for the request window.Also contains utilities and examples for convenient use with FastAPI.InstallationIt's a Python library, what do you expect?pipinstallpyredlight# ORpoetryaddpyredlightUsageIf you want to test the examples make sure you replaceredis://your.redis.serverwith an actual connect string.Limits can be defined using the format{requests}/{time}[hms], so100/10s= 100 requests / 10 seconds,60/1m= 60
requests / minute,5000/6h= 5000 requests / 6 hours.Small example of how you can use this library (also inexample.py, test
withpoetry install && poetry run python example.py):importasyncioimportredis.asyncioasredisfrompyredlightimportlimit,set_redisrequests_per_minute=limit("60/60s")defget_key(request):returnf"rate_limit_example_{request['client_ip']}"asyncdefhandle_request(request):key=get_key(request)ok,remaining,expires=awaitrequests_per_minute.is_ok(key)ifnotok:return{"status":429,"rate_limit_remaining":remaining,# Always 0"rate_limit_expires":expires,}else:return{"status":200,"rate_limit_remaining":remaining,"rate_limit_expires":expires,}asyncdefmain():for_inrange(10):print(awaithandle_request({"client_ip":"127.0.0.1"}))if__name__=="__main__":set_redis(redis.from_url("redis://your.redis.server"))asyncio.run(main())Or in FastAPI (also infastapi_example, test
withcd fastapi_example && poetry install && poetry run python example.py):fromfastapiimportFastAPI,Request,APIRouter,Dependsfromstarlette.responsesimportJSONResponsefrompyredlightimportlimit,set_redisfrompyredlight.fastapiimportmake_dependsimportredis.asyncioasredisper_minute_limit=limit("60/60s")defget_rate_limit_key(request:Request):returnrequest.client.host+"custom"per_minute_depend=make_depends(per_minute_limit)custom_key_example=make_depends(per_minute_limit,get_key=get_rate_limit_key)router=APIRouter()@router.get("/")asyncdefget_data(_=Depends(per_minute_depend)):returnJSONResponse({"status":"ok"})@router.post("/")asyncdefset_data(_=Depends(custom_key_example)):returnJSONResponse({"status":"ok"})app=FastAPI()app.include_router(router)@app.middleware("http")asyncdefrate_limit_headers(request:Request,call_next):response=awaitcall_next(request)rate_limit_remaining=request.scope.get("rate_limit_remaining",None)ifrate_limit_remainingisnotNone:response.headers["X-Rate-Limit-Remains"]=str(request.scope["rate_limit_remaining"])response.headers["X-Rate-Limit-Expires"]=str(request.scope["rate_limit_expires"])returnresponse@app.on_event("startup")asyncdefsetup():set_redis(redis.from_url("redis://your.redis.server"))For the rare cases you might want to reset a limit, you can also.clear(key)instead of.is_ok(key).You may also occasionally find it useful to merge limits, to e.g. check20/1sand50/1mat the same time, for that
there's a helper:frompyredlightimportlimit,merge_limitsper_second_limit=limit("20/1s")per_minute_limit=limit("50/1m")combined_limit=merge_limits([per_second_limit,per_minute_limit])asyncdefexample():ok,remaining,expires=awaitcombined_limit.is_ok("key")The return value frommerge_limitsimplements the same interface aslimit, so you can also.clear()keys - which
will then be cleared from all the limits.PerformanceYou should really not expect much extra latency beyond a single network RTT to your Redis server for each check, as long
as your Redis server is capable of handling the requests. With a very simple Redis server in the same LAN
asbenchmark.pyit seems each call is taking approx 110-150μsec.DevelopmentIssues and PRs are welcome!Please open an issue first to discuss the idea before sending a PR so that you know if it would be wanted or needs
re-thinking or if you should just make a fork for yourself.For local development, make sure you installpre-commit, then run:pre-commitinstall
poetryinstall
poetryrunpytest-watch
poetryrunpythonexample.pycdfastapi_example
poetryrunpythonexample.pyLicenseThe code is released under the BSD 3-Clause license. Details in theLICENSE.mdfile.Financial supportThis project has been made possible thanks toCocreatorsandLietu. You
can help us continue our open source work by supporting us
onBuy me a coffee.