mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-16 06:03:26 -05:00
[PR #24633] [CLOSED] feat: backend-only, config-driven, per-user endpoint rate limiting middleware #131415
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24633
Author: @Srujan4812
Created: 5/12/2026
Status: ❌ Closed
Base:
dev← Head:feature/rate-limit-middleware📝 Commits (10+)
fe6783cMerge pull request #19030 from open-webui/devfc05e0aMerge pull request #19405 from open-webui/deve3faec6Merge pull request #19416 from open-webui/dev9899293Merge pull request #19448 from open-webui/dev140605eMerge pull request #19462 from open-webui/dev6f1486fMerge pull request #19466 from open-webui/devd95f533Merge pull request #19729 from open-webui/deva7271530.6.43 (#20093)6adde20Merge pull request #20394 from open-webui/devf9b0534Merge pull request #20522 from open-webui/dev📊 Changes
4 files changed (+923 additions, -0 deletions)
View changed files
📝
backend/open_webui/config.py(+86 -0)📝
backend/open_webui/main.py(+31 -0)➕
backend/open_webui/test/util/test_rate_limit_middleware.py(+407 -0)➕
backend/open_webui/utils/rate_limit_middleware.py(+399 -0)📄 Description
Summary
Implements the backend-only first phase of the rate-limiting primitive proposed in #24631. Adds a pure-ASGI middleware that applies per-user, per-endpoint-class limits across four endpoint classes (
chat,embeddings,uploads,admin), backed by the existing Redis connection andAppConfigplumbing.Feature is disabled by default (
ENABLE_RATE_LIMITING=false). Enabling it is additive — no response semantics change for requests under the limit.Closes #24631 (first phase).
Scope (intentionally narrow)
In scope:
backend/open_webui/utils/rate_limit_middleware.pymain.pybetweenAuthTokenMiddlewareandWebsocketUpgradeGuardMiddlewarePersistentConfigfields inconfig.pyunderrate_limiting.*backend/open_webui/test/util/test_rate_limit_middleware.py(no new test dependency — follows theAsyncMockpattern fromtest_redis.py)Explicitly out of scope (deferred to follow-ups, as outlined in the RFC):
src/changes)/statsendpoint or runtime introspection APIsignin_rate_limiterDesign
Endpoint classification
A compiled regex table at module import time — ships hard-coded, overridable in a follow-up. First match wins.
chat/api/*/chat/completions,/ollama/api/chat,/ollama/api/generate,/openai/chat/completionsembeddings/api/*/embeddingsuploads/api/v1/files,/api/v1/retrieval/process,/api/v1/knowledge/*/file/addadmin/api/v1/configs|models|groups|users— write methods only (GET/HEAD/OPTIONS are not rate-limited)Skip list (never rate-limited):
/health,/ready,/ws/*,/static/*,/assets/*,/cache/*,/_app/*, and the SPA root.Identity resolution
Runs after
AuthTokenMiddlewaresorequest.state.tokenis already parsed. Priority:idclaim viautils.auth.decode_token. No DB lookup. Revocation and user-existence checks happen inget_current_userdownstream.sk-…) — SHA-256 first 16 hex chars. Raw key never appears in Redis.Algorithm
Sliding-window log via a single Lua script on a Redis sorted set — one atomic
EVALSHAper classified request, withEVALfallback onNOSCRIPT. Sorted-set size is bounded bylimit + 1entries per active(identity, class)tuple and aggressively cleaned withZREMRANGEBYSCORE. 50 ms timeout on every Redis call.Key layout
id_type∈{u, k, i}(user / key-hash / IP). Scoping by class lets operators tune each surface independently.Failure semantics
429 Too Many Requests, JSON body{"detail": "Rate limit exceeded for <class>", "class": "<class>"}, plus standardRateLimit-Limit/RateLimit-Remaining/RateLimit-Reset/Retry-Afterheaders.RATE_LIMITING_FAIL_OPEN=true). Configurable; rationale is that a Redis incident must not take the app offline. Logged at DEBUG to avoid log floods under attack.Middleware placement
Sits after
AuthTokenMiddleware(readsrequest.state.tokenwithout re-parsing), beforeWebsocketUpgradeGuardMiddlewareandCORSMiddleware(short-circuits abusive traffic before WS/Engine.IO negotiation or CORS preflight costs).Config fields
All hot-reloadable via
AppConfigunderrate_limiting.*. Defaults are conservative so operators who flip the switch don't get surprise 429s on normal usage.ENABLE_RATE_LIMITINGfalseRATE_LIMITING_FAIL_OPENtrueRATE_LIMITING_CHAT_LIMIT/_WINDOW30/60sRATE_LIMITING_EMBEDDINGS_LIMIT/_WINDOW20/60sRATE_LIMITING_UPLOADS_LIMIT/_WINDOW10/60sRATE_LIMITING_ADMIN_LIMIT/_WINDOW60/60sSetting any per-class limit to
0disables that class without any Redis traffic.Testing
20 unit tests in
test/util/test_rate_limit_middleware.pycovering:classify()cases — all four classes plus the skip list, with admin honouring the write-method filter.resolve_identity()cases — JWT > API key hash > client IP >unknown.ENABLE_RATE_LIMITING=false.RateLimit-*headers.Retry-After, correct content-type, and structured body.ConnectionError; fail-closed on the same condition when configured.NOSCRIPTfallback path exercisesEVAL.{prefix}:rl:{class}:{id_type}:{id}.Tests use
unittest.mock.AsyncMockto mirrortest_redis.py— no new third-party dependency.Compatibility
/health,/ready,/ws/*, static assets — probes and WebSocket traffic are unaffected.ENABLE_RATE_LIMITING=true(and even then fails open if Redis is unreachable).Follow-ups (not in this PR)
Tracked against #24631:
openwebui.ratelimit.hits{class,outcome}) and a minimal/api/v1/configs/rate-limitsread endpoint.src/routes/(app)/admin/settings/.signin_rate_limiteronto this primitive.Each follow-up is a pure addition to the primitive landed in this PR — no refactor required.
Checklist
ruff checkandruff format --checkutils/asgi_middleware.pyapp.state.redisasync clientREDIS_KEY_PREFIXso multi-tenant / multi-env Redis is safe🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.