minimost.ratelimit
minimost.ratelimit
Dependency-free, in-process abuse throttling for MiniMost.
This module provides the two primitives the application uses to blunt denial-of-service (DoS) attempts from authenticated and unauthenticated clients alike, using nothing beyond the Python standard library and Flask:
RateLimiter— a thread-safe sliding-window counter. Each call toRateLimiter.hit()records one event against a key (an IP address or a username) and reports whether that key has exceeded its allowance over the trailing window. Therate_limit()decorator wraps a Flask view with this check so a flood of requests to expensive endpoints (login, signup, message send, uploads) is rejected with429 Too Many Requestsinstead of tying up workers or CPU.ConcurrencyLimiter— a thread-safe count of currently held resources per key. It backs the cap on simultaneous Server-Sent Events streams per user (minimost.events), so one actor cannot pin every worker thread by opening connections that never close.
Scope and limitations
The counters live in the worker process, so with several Gunicorn workers
the effective ceiling is limit × workers. That is deliberate: the goal is
to stop pathological abuse cheaply with zero new dependencies and no shared
store on the hot path, not to enforce a globally exact quota. For a hard,
cross-worker ceiling, put a reverse proxy in front (e.g. Nginx limit_req /
limit_conn); these in-process limits complement that layer rather than
replace it.
Configuration
Limits are generous by default — tuned so ordinary interactive use never trips
them — and can be overridden in settings.json (re-read, cached by mtime, so
edits take effect without a restart):
rate_limit_enabled— master on/off switch (defaulttrue). Read once inminimost.create_app()intoapp.config["RATELIMIT_ENABLED"].rate_limits— an object mapping an action name to a[max, window_seconds]pair, overriding the matching entry inDEFAULT_LIMITS.max_event_streams_per_user— the per-user concurrent/eventscap (defaultDEFAULT_MAX_EVENT_STREAMS).
- class minimost.ratelimit.RateLimiter[source]
Bases:
objectA thread-safe sliding-window rate limiter keyed by arbitrary strings.
Each key maps to a
collections.dequeof monotonic timestamps for the events seen within the trailing window; expired timestamps are evicted lazily on the nexthit(). All state is in-process and guarded by a single lock, so the limiter is safe to share across Gunicorngthreadworker threads.- hit(key, limit, window)[source]
Record one event for key and report whether it is allowed.
- Parameters:
key – The identity being limited (e.g.
"login:1.2.3.4").limit – Maximum number of events permitted within window.
window – Trailing window length in seconds.
- Returns:
(allowed, retry_after)— allowed isFalsewhen the key has already reached limit events in the window (the event is not recorded in that case), and retry_after is a whole-second hint for theRetry-Afterheader (0when allowed).- Return type:
- class minimost.ratelimit.ConcurrencyLimiter[source]
Bases:
objectA thread-safe count of currently-held resources per key.
Unlike
RateLimiter, which counts events over time, this tracks how many resources (e.g. open SSE streams) a key holds right now:acquire()takes a slot if one is free andrelease()returns it.
- minimost.ratelimit._load_settings()[source]
Return parsed
settings.json, cached by file mtime.Any read or parse error yields an empty dict so callers fall back to the built-in defaults.
- minimost.ratelimit._is_number(value)[source]
True for a real int/float (rejecting
bool, which is anint).
- minimost.ratelimit.limit_for(name)[source]
Return the
(max_events, window_seconds)limit for action name.Falls back to
DEFAULT_LIMITSwhen no valid override is present in therate_limitsobject ofsettings.json.
- minimost.ratelimit.max_event_streams()[source]
Return the per-user concurrent
/eventscap from settings (or default).
- minimost.ratelimit._enabled()[source]
True unless the app has disabled limiting via
RATELIMIT_ENABLED.
- minimost.ratelimit.client_ip()[source]
Return the remote address for the current request, or
"unknown".Behind a reverse proxy this is the proxy’s address unless the proxy is configured to forward the real client IP and the app is set up to trust it; see the deployment docs.
- minimost.ratelimit.rate_limit_message(retry_after)[source]
Return a friendly ‘slow down’ message naming the wait in whole seconds.
Shared by every throttled response (plain text, rendered HTML, and JSON) so the wording the user sees is identical however the route reports the limit.
- minimost.ratelimit._too_many(retry_after)[source]
Build the default plain-text
429response with aRetry-Afterheader.
- minimost.ratelimit.rate_limit(name, by='ip', on_limit=None)[source]
Decorate a Flask view so calls to it are throttled per name.
- Parameters:
name – Key into
DEFAULT_LIMITS(and therate_limitssettings override) selecting this endpoint’s allowance.by –
"ip"to key the limit on the client IP (for unauthenticated endpoints like login/signup), or"user"to key it on the logged-in username, falling back to the IP when there is no session.on_limit – Optional callable
on_limit(retry_after)returning the Flask response to send when the limit is hit, instead of the default plain-text429. Use it to surface the limit the way the caller expects — a re-rendered HTML form with an inline error, or a JSON error for afetchendpoint.rate_limit_message()builds the standard wording, and callers should keep the429status andRetry-Afterheader (e.g.(render_template(...), 429, {"Retry-After": str(r)})).
- Returns:
The wrapped view, which returns
429once the limit is hit and otherwise calls through unchanged. Limiting is skipped entirely whenapp.config["RATELIMIT_ENABLED"]is false.
Apply it below
minimost.auth.login_required()so authenticatedby="user"endpoints see the established session:@chat_bp.route("/send/<channel>", methods=["POST"]) @auth.login_required @ratelimit.rate_limit("send", by="user") def send(channel): ...
- minimost.ratelimit.acquire_stream(user)[source]
Try to take an
/eventsstream slot for user.- Returns:
Trueif the user is below the concurrent-stream cap (a slot is now held and must be released withrelease_stream()),Falseif they are already at the cap.- Return type: