Apache APISIX plugin for three-tier rate limiting used by Ecosyste.ms.
This plugin categorizes requests into three tiers with different rate limits:
- API Key - Users with API keys get the highest limits
- Polite - Users who include an email in their User-Agent or use the
mailto
parameter get moderate limits - Anonymous - Everyone else gets the most restrictive limits
The plugin identifies and tracks requests differently based on the tier:
-
API Key Tier: When an API key is provided (via header or query parameter), the rate limit is tracked per API key. Each unique API key has its own quota, regardless of the IP address making the request. This means multiple users or servers can share an API key and share the same rate limit quota.
-
Polite Tier: When an email address is detected (in the User-Agent header or
mailto
query parameter), the request is classified as "polite" but is still tracked by IP address. The email only determines which tier's limits apply - it does not become the identifier. Each unique IP address gets its own polite tier quota. -
Anonymous Tier: All other requests are tracked by IP address with the anonymous tier limits.
- Same IP with email → Gets polite tier limits, tracked by that IP
- Same IP with API key → Gets API key tier limits, tracked by that specific key
- Different IPs with same email → Each IP gets their own separate polite tier quota
- Different IPs with same API key → Share the same API key quota
Requests to specific host domains can bypass rate limiting entirely. By default, grafana.ecosyste.ms
, prometheus.ecosyste.ms
, and apisix.ecosyste.ms
are exempt. This ensures these services never get rate limited.
Exempt requests bypass rate limiting completely and don't receive rate limit headers.
- Clone the repository and copy the plugin to your APISIX container:
git clone https://github.com/ecosyste-ms/conditional-rate-limit.lua
docker cp ~/conditional-rate-limit.lua/conditional-rate-limit.lua apisix-quickstart:/usr/local/apisix/apisix/plugins
- Restart APISIX to load the new plugin:
docker restart apisix-quickstart
- Configure as a global rule via the APISIX Admin API:
curl -X PUT \
http://YOUR_APISIX_IP:9180/apisix/admin/global_rules/1 \
-H 'Content-Type: application/json' \
-H "X-API-KEY: YOUR_ADMIN_API_KEY" \
-d '{
"plugins": {
"conditional-rate-limit": {
"api_key_count": 50000,
"api_key_time_window": 3600,
"polite_count": 15000,
"polite_time_window": 3600,
"anonymous_count": 5000,
"anonymous_time_window": 3600,
"key_header": "X-API-Key",
"key_query_param": "apikey",
"mailto_query_param": "mailto",
"email_pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
}
}
}'
plugins:
conditional-rate-limit:
enable: true
config:
# API Key tier
api_key_count: 1000
api_key_time_window: 60
# Polite tier
polite_count: 100
polite_time_window: 60
# Anonymous tier
anon_count: 10
anon_time_window: 60
# API key detection
key_header: "X-API-Key"
key_query_param: "apikey"
# Email detection for polite tier
mailto_query_param: "mailto"
email_pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
# Response
rejected_code: 429
rejected_msg: "Too many requests"
# Exemptions (optional)
exempt_hosts: # Defaults to ["grafana.ecosyste.ms", "prometheus.ecosyste.ms", "apisix.ecosyste.ms"]
- "grafana.ecosyste.ms"
- "prometheus.ecosyste.ms"
- "apisix.ecosyste.ms"
# API key - 1000 req/min
curl -H "X-API-Key: your-key" https://api.ecosyste.ms/endpoint
# Polite - 100 req/min (via User-Agent)
curl -H "User-Agent: MyApp/1.0 (contact: user@example.com)" https://api.ecosyste.ms/endpoint
# Polite - 100 req/min (via mailto parameter)
curl "https://api.ecosyste.ms/endpoint?mailto=you@example.com"
# Anonymous - 10 req/min
curl https://api.ecosyste.ms/endpoint
GNU Affero General Public License v3.0 - see LICENSE file.