v1.86. Every handler supports rate-limiting via the ratelimit
config.
For example, this allows 50 hits per user per day:
url:
api:
pattern: /api
handler: FormHandler # or any handler
kwargs:
# ...
ratelimit:
keys: [daily, user]
limit: 50
Rate limit example
keys
define how to rate-limit. It’s an array of strings, or a comma-separated list of strings:
# Set a weekly limit by user
keys: [weekly, user]
# Another way to set weekly limit by user
keys: weekly, user
# Set a daily limit globally
keys: [daily]
keys
can be:
hourly
: Reset limit every hour (UTC)daily
: Reset limit every day (UTC)weekly
: Reset limit every week, starting Sunday (UTC)monthly
: Reset limit every month, starting 1st of the month (UTC)yearly
: Reset limit every month, starting 1-Jan (UTC)user
: The unique ID of the user requesting the page (same as user.id
)uri
: The full URL requested (after the host name)method
: The HTTP method requested. E.g. GET
or POST
ip
: The IP address of the client requesting the pageuser.<key>
: A user attribute. E.g. user.id
returns the user ID, user.role
might point to a roleargs.<key>
: A specific argument. E.g. args.x
returns the value of ?x=…headers.<key>
: A request HTTP header. E.g. headers.User-Agent
is the browser’s user agentsession.<key>
: A HTTP session key. E.g. session.user
is the user objectcookies.<key>
: A specific cookie. E.g. cookie.sid
is the session ID cookieenv.<key>
: An environment variable. E.g. env.HOME
logs the user’s home directorykeys
can also be defined with functions. For example:
keys:
# Restrict by user's email domain name
- function: handler.current_user.email.split('@')[-1]
# Refresh every 10 days
- function: pd.Timestamp.utcnow().ceil(freq='10D').isoformat()
expiry: int((pd.Timestamp.utcnow().ceil(freq='10D') - pd.Timestamp.utcnow()).total_seconds())
# Refresh every 30 minutes
- function: pd.Timestamp.utcnow().ceil(freq='30T').isoformat()
expiry: int((pd.Timestamp.utcnow().ceil(freq='30T') - pd.Timestamp.utcnow()).total_seconds())
function
returns the keyexpiry
returns the seconds to expiry. If this is not set, the key never expires.v1.92. Use key: expression
to define a time-based keys like hourly
, daily
, weekly
, monthly
or yearly
:
keys:
# Set different frequencies for different users
- key: 'daily' if handler.current_user['role'] == 'admin' else 'monthly'
limit
is the maximum number of successful requests to the page. This can be a number, or a function that returns a number. For example:
# Set a constant limit of 50
limit: 50
# Limit to 50 for logged-in users, 10 for others
limit: {function: 50 if handler.current_user else 10}
On each request, Gramex computes the keys
and limit
, looks up usage for that key, and sets these HTTP headers:
X-RateLimit-Limit
: limitX-RateLimit-Remaining
: limit minus usageX-RateLimit-Reset
: seconds before window resets. Only available for hourly
, daily
, weekly
and monthly
keysRetry-After
: same as X-Ratelimit-Reset
, sent only if usage exceeds limitIf the usage exceeds the limit, Gramex raises a HTTP 429 Too Many Requests response. This can be formatted via a custom error page.
If the response is successful, the usage increments by 1. If the response is a HTTP 5xx or HTTP 4xx, usage stays the same.
If a single API has multiple URLs (e.g. /api1
, /api2
, etc), add the same ratelimit.pool:
to all. This combines their usage. For example:
url:
page1:
pattern: /api1
handler: FormHandler # or any handler
kwargs:
# ...
ratelimit: &API_POOL
pool: my-api-pool
keys: [daily, user]
limit: 50
page2:
pattern: /api2
handler: FormHandler # or any handler
kwargs:
# ...
ratelimit:
<<: *API_POOL # Copy config from earlier
Calling /api1
and api2
increase the SAME usage counter by 1.
To reset the API usage for any key, call handler.ratelimit_reset(pool, keys)
from any
FunctionHandler.
For example, this sets the usage of x@example.com
on 2022-01-01
to zero.
handler.ratelimit_reset('my-api-pool', ['2022-01-01', 'x@example.com'])
Rate limit usage data is stored in a cache.
It’s location is defined in app.ratelimit
in Gramex’s own gramex.yaml
:
app:
ratelimit:
# Save in a JSON store
type: json
path: $GRAMEXDATA/ratelimit.json
# Flush every 30 seconds. Clear expired sessions every hour
flush: 30
purge: 3600
This configuration works exactly like sessions. To use a Redis store, use:
app:
ratelimit:
type: redis
path: localhost:6379:1 # Redis server:port:db (default: localhost:6379:0)
# flush: 30 # Redis stores are live. No flush required
purge: 3600
v1.91. You can access rate limits for the current request via handler.get_ratelimit()
. This
returns a rate limit object like this:
{
"limit": 3, # the limit on the rate limit
"usage": 2, # the usage so far (BEFORE current request, e.g. 0, 1, 2, 3, ...)
"remaining": 0, # remaining requests (AFTER current request, e.g. 2, 1, 0, 0, ...)
"expiry": 103, # seconds to expiry for this rate limit
}
If there are multiple rate limits, it picks the one with least remaining usage.
v1.91. ratelimit
can be an array of rate limit configurations. For example, to set 2 limit:
ratelimit:
- pool: daily-user-pool
keys: [daily, user]
limit: 30
- pool: daily-pool
keys: [daily]
limit: 100
daily-user-pool
becomes 9, and the daily-pool
becomes 99daily-user-pool
becomes 9, and the daily-pool
becomes 98daily-user-pool
becomes 8, and the daily-pool
becomes 97The Rate limit headers are computed from the smallest remaining pool.
daily-user-pool
has 8 remaining (less than daily-pool
with 97). So daily-user-pool
is used and X-RateLimit-Remaining
header is 8, not 97.daily-user-pool
has 8 remaining but daily-pool
as only 5, the X-RateLimit-Remaining
header would be 7.
X-Ratelimit-Limit
and X-Ratelimit-Reset
are computed from the daily-pool
.You can reset rate limits for each pool independently.