Rate Limit and Quota
EnerOS implements layered rate limiting through eneros-gateway to protect kernel stability and enable fair multi-tenant scheduling. The rate limiting policy covers four dimensions: IP, tenant, user, and endpoint, and can be adjusted in configuration files with quotas dynamically distributed at runtime via eneros-tenant. All rate-limited responses include standard X-RateLimit-* response headers, enabling clients to implement adaptive rate reduction.
Rate Limiting Architecture
EnerOS adopts a four-layer rate limiting model, with each layer having independent rate and burst limits:
Request → ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ IP Limit │ → │ Tenant │ → │ User │ → │ Endpoint │ → Kernel
│ │ │ Limit │ │ Limit │ │ Limit │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Token Bucket Token Bucket Token Bucket Token Bucket
| Layer | Dimension | Algorithm | Purpose |
|---|---|---|---|
| L1 | Client IP | Token Bucket | Prevent malicious API abuse, DDoS mitigation |
| L2 | Tenant | Token Bucket | Multi-tenant fair scheduling, quota management |
| L3 | User/Agent | Token Bucket | Per-user rate limiting, abuse prevention |
| L4 | Endpoint | Leaky Bucket | Protect compute-intensive endpoints |
Each layer counts independently. If any layer is triggered, 429 is returned, with the response header carrying detailed information about the triggered layer.
Rate Limiting Policies
1. Global IP Rate Limiting
Counted by IP dimension, prevents malicious API abuse and DDoS attacks. All IPs share the same policy, with blacklisted IPs directly rejected.
| Endpoint Type | Rate | Burst | Description |
|---|---|---|---|
Public endpoints (/health) | 60 req/min | 10 | No authentication required |
Authentication endpoints (/auth/token) | 30 req/min | 5 | Brute force prevention |
| Authenticated endpoints (other) | 600 req/min | 50 | Default |
Compute endpoints (/powerflow, /opf) | 30 req/min | 5 | Protect compute capacity |
Time series write (/timeseries/batch) | 1000 req/min | 100 | High throughput |
| WebSocket connection establishment | 10 req/min | 2 | Prevent connection flooding |
| SSE connection establishment | 20 req/min | 5 | Prevent connection flooding |
2. Tenant Quota
Resource quotas allocated by tenant dimension, exceeding limits returns 429 or queues for waiting. Quotas can be configured in eneros.toml or dynamically adjusted via eneros-tenant.
| Resource | Default Quota | Exceed Behavior | Adjustable |
|---|---|---|---|
| API requests | 5000 req/h | Queue (max 100) | Yes |
| WebSocket connections | 20 concurrent | Reject new connections | Yes |
| SSE connections | 50 concurrent | Reject new connections | Yes |
| Power flow calculation tasks | 100 concurrent | Queue (FIFO) | Yes |
| Time series writes | 1M points/day | Reject writes | Yes |
| Time series storage | 10 GB/day | Reject writes | Yes |
| Audit log retention | 365 days | Auto archive | No |
| Agent count | 100 | Reject creation | Yes |
| Network model count | 50 | Reject creation | Yes |
3. User/Agent Rate Limiting
Rate limiting for individual users or Agents, preventing a single entity from consuming too many resources.
| Resource | Default Limit | Description |
|---|---|---|
| API requests per user | 1200 req/min | Regular user |
| Command frequency per Agent | 10 cmd/s | Prevent Agent runaway |
| WebSocket per user | 5 concurrent | — |
| SSE per user | 10 concurrent | — |
4. Endpoint Rate Limiting
Fine-grained rate limiting for specific endpoints, protecting compute-intensive or high-resource-consumption operations.
| Endpoint | Rate | Burst | Description |
|---|---|---|---|
POST /networks/{id}/powerflow | 30 req/min | 5 | Power flow calculation |
POST /networks/{id}/opf | 10 req/min | 2 | Optimal power flow |
POST /networks/{id}/validate | 60 req/min | 10 | Topology validation |
POST /agents/{id}/command | 100 req/min | 20 | Agent command |
POST /timeseries/batch | 1000 req/min | 100 | Batch write |
DELETE /networks/{id} | 10 req/min | 2 | Delete protection |
5. Command Rate Limiting
Agent command dispatch is subject to secondary validation by the safety gateway:
| Command Type | Limit | Description |
|---|---|---|
| Normal commands | ≤ 10 cmd/s | Per Agent |
| Control commands (switch/adjust) | 100ms minimum interval + two-factor confirmation | High-risk operations |
| Emergency shutdown commands | Bypass rate limit, execute immediately | Emergency scenarios |
| Batch commands | ≤ 50 cmd/batch | Single batch limit |
Response Header Description
All rate-limit-related response headers follow the IETF draft draft-ietf-httpapi-ratelimit-headers:
| Response Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | int | Maximum requests allowed in the current window |
X-RateLimit-Remaining | int | Remaining requests in the current window |
X-RateLimit-Reset | int | Window reset time (Unix timestamp, seconds) |
X-RateLimit-Policy | string | Rate limit policy description, e.g., 60;w=60 means 60 times in a 60-second window |
Retry-After | int | Suggested retry wait seconds (429 responses only) |
X-RateLimit-Tenant-Limit | int | Tenant quota limit |
X-RateLimit-Tenant-Remaining | int | Tenant quota remaining |
X-RateLimit-Concurrent | int | Current concurrent connections (WS/SSE only) |
Success response example:
HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1783277460
X-RateLimit-Policy: 600;w=60
X-RateLimit-Tenant-Limit: 5000
X-RateLimit-Tenant-Remaining: 4998
Content-Type: application/json
Rate Limit Response
When rate limiting is triggered, HTTP 429 is returned, with the response body containing detailed rate limit information:
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded: 600 req/min",
"details": {
"limit": 600,
"window_seconds": 60,
"retry_after": 12,
"reset_at": "2026-07-06T08:31:00Z",
"scope": "ip",
"ip": "192.168.1.10"
},
"trace_id": "trace_abc123"
},
"request_id": "req_20260706_001"
}
| Field | Type | Description |
|---|---|---|
error.code | string | Fixed as RATE_LIMITED |
error.details.limit | int | Maximum requests allowed in the current window |
error.details.window_seconds | int | Rate limit window (seconds) |
error.details.retry_after | int | Suggested retry wait seconds |
error.details.reset_at | string | Window reset time (RFC 3339) |
error.details.scope | string | Triggered layer: ip/tenant/user/endpoint |
error.details.ip | string | Triggered IP (scope=ip only) |
error.details.tenant_id | string | Triggered tenant (scope=tenant only) |
Quota Management
Query Current Quota
Query the current tenant’s quota and usage via GET /api/v1/tenant/quota:
Request Parameters: None
Response Fields:
| Field | Type | Description |
|---|---|---|
tenant_id | string | Tenant ID |
plan | string | Plan: free/pro/enterprise |
quotas | object | Quota limits for each item |
usage | object | Current usage |
reset_at | string | Quota reset time |
cURL Example:
curl http://localhost:8080/api/v1/tenant/quota \
-H "Authorization: Bearer <token>"
Response Example:
{
"data": {
"tenant_id": "tenant_default",
"plan": "pro",
"quotas": {
"api_requests_per_hour": 5000,
"ws_connections": 20,
"sse_connections": 50,
"concurrent_powerflow": 100,
"timeseries_daily_gb": 10,
"agent_count": 100,
"network_count": 50
},
"usage": {
"api_requests_this_hour": 1234,
"ws_connections": 3,
"sse_connections": 12,
"concurrent_powerflow": 2,
"timeseries_today_gb": 2.3,
"agent_count": 15,
"network_count": 8
},
"reset_at": "2026-07-06T09:00:00Z"
},
"request_id": "req_20260706_002",
"timestamp": "2026-07-06T08:30:00Z"
}
Quota Application
When the default quota is insufficient, you can apply for an increase through the following methods:
| Plan | API Requests | WS Connections | SSE Connections | Time Series Storage | Applicable Scenarios |
|---|---|---|---|---|---|
free | 1000/h | 5 | 10 | 1 GB/day | Trial, development |
pro | 5000/h | 20 | 50 | 10 GB/day | Small production |
enterprise | 50000/h | 100 | 200 | 100 GB/day | Large production |
custom | Custom | Custom | Custom | Custom | Special requirements |
Application process:
- Contact EnerOS sales team (sales@openeneros.com)
- Provide tenant ID, expected usage, business scenario
- Sign supplementary agreement
- Operations dynamically distributes new quota via
eneros-tenant, no restart required
Dynamic Quota Adjustment
Administrators can dynamically adjust tenant quotas via API:
curl -X PUT http://localhost:8080/api/v1/admin/tenants/tenant_default/quota \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"api_requests_per_hour": 10000,
"ws_connections": 50,
"concurrent_powerflow": 200,
"reason": "promotion_campaign",
"expires_at": "2026-07-13T00:00:00Z"
}'
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
api_requests_per_hour | int | No | — | API request quota |
ws_connections | int | No | — | WebSocket connection quota |
sse_connections | int | No | — | SSE connection quota |
concurrent_powerflow | int | No | — | Concurrent power flow tasks |
timeseries_daily_gb | int | No | — | Time series storage quota |
reason | string | Yes | — | Adjustment reason (for audit) |
expires_at | datetime | No | — | Temporary quota expiration time |
Configuration Example
Configure rate limiting policies in eneros.toml:
[gateway.rate_limit]
# Global IP rate limiting
global_rpm = 600 # Max requests per minute
global_burst = 50 # Burst limit
auth_rpm = 30 # Authentication endpoints
auth_burst = 5
compute_rpm = 30 # Compute endpoints
compute_burst = 5
timeseries_rpm = 1000 # Time series write
timeseries_burst = 100
# Tenant quota defaults
[gateway.tenant_default]
api_requests_per_hour = 5000
ws_connections = 20
sse_connections = 50
concurrent_powerflow = 100
timeseries_daily_gb = 10
agent_count = 100
network_count = 50
# Endpoint-level rate limiting
[gateway.endpoint_limits]
"/networks/{id}/powerflow" = { rpm = 30, burst = 5 }
"/networks/{id}/opf" = { rpm = 10, burst = 2 }
"/agents/{id}/command" = { rpm = 100, burst = 20 }
# IP blacklist/whitelist
[gateway.ip_filter]
whitelist = ["127.0.0.1", "10.0.0.0/8"]
blacklist = []
blacklist_ttl_seconds = 3600
# WebSocket rate limiting
[gateway.ws]
max_connections_per_tenant = 20
max_connections_per_user = 5
max_subscriptions_per_connection = 50
message_rate_per_second = 100
# SSE rate limiting
[gateway.sse]
max_connections_per_tenant = 50
max_connections_per_user = 10
max_topics_per_connection = 10
Client Best Practices
1. Exponential Backoff Retry
import time
import random
import requests
def request_with_retry(url, headers, max_retries=5):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
# Add jitter to avoid retry storms
jitter = random.uniform(0, 0.5)
sleep_time = retry_after + jitter
print(f"Rate limited, retrying in {sleep_time:.1f}s (attempt {attempt + 1})")
time.sleep(sleep_time)
continue
return response
raise Exception(f"Max retries reached: {max_retries}")
2. Adaptive Rate Reduction
import time
import requests
class AdaptiveClient:
def __init__(self, base_url, token):
self.base_url = base_url
self.headers = {"Authorization": f"Bearer {token}"}
self.min_interval = 0.01 # 10ms
self.max_interval = 1.0 # 1s
self.current_interval = self.min_interval
def request(self, path):
time.sleep(self.current_interval)
response = requests.get(f"{self.base_url}{path}", headers=self.headers)
remaining = int(response.headers.get("X-RateLimit-Remaining", 100))
if remaining < 10:
# Insufficient quota remaining, slow down
self.current_interval = min(self.current_interval * 1.5, self.max_interval)
else:
# Sufficient quota, speed up
self.current_interval = max(self.current_interval * 0.9, self.min_interval)
return response
3. Batch Requests to Reduce Call Count
# Wrong: write one by one
for point in points:
client.timeseries.write(point) # 1000 requests
# Correct: batch write
client.timeseries.batch_write(points) # 1 request
4. Long Connection Reuse
# Wrong: repeatedly establish WebSocket connections
for alert in alerts:
ws = connect_websocket()
ws.send(alert)
ws.close()
# Correct: reuse long connection
ws = connect_websocket()
for alert in alerts:
ws.send(alert)
# Connection maintained, subscribed to events
5. Monitor Remaining Quota
response = client.get("/networks")
remaining = response.headers.get("X-RateLimit-Remaining")
tenant_remaining = response.headers.get("X-RateLimit-Tenant-Remaining")
if int(remaining) < 50:
logger.warning(f"IP rate limit about to trigger, remaining: {remaining}")
if int(tenant_remaining) < 500:
logger.warning(f"Tenant quota about to be exhausted, remaining: {tenant_remaining}")
Rust Client Example
use std::time::{Duration, Instant};
use tokio::time::sleep;
pub struct RateLimitedClient {
client: reqwest::Client,
base_url: String,
token: String,
last_request: Instant,
min_interval: Duration,
}
impl RateLimitedClient {
pub fn new(base_url: &str, token: &str, rpm: u32) -> Self {
Self {
client: reqwest::Client::new(),
base_url: base_url.to_string(),
token: token.to_string(),
last_request: Instant::now() - Duration::from_secs(1),
min_interval: Duration::from_secs(60) / rpm,
}
}
pub async fn get(&mut self, path: &str) -> anyhow::Result<reqwest::Response> {
let elapsed = self.last_request.elapsed();
if elapsed < self.min_interval {
sleep(self.min_interval - elapsed).await;
}
self.last_request = Instant::now();
let resp = self.client
.get(format!("{}{}", self.base_url, path))
.bearer_auth(&self.token)
.send()
.await?;
if resp.status() == 429 {
let retry_after = resp.headers()
.get("Retry-After")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(60);
sleep(Duration::from_secs(retry_after)).await;
return self.get(path).await;
}
Ok(resp)
}
}
Monitoring and Alerting
EnerOS exposes the following rate limiting metrics, which can be scraped via Prometheus:
| Metric | Type | Description |
|---|---|---|
eneros_rate_limit_requests_total | counter | Total requests (by status, scope dimensions) |
eneros_rate_limit_rejected_total | counter | Requests rejected by rate limiting |
eneros_rate_limit_remaining | gauge | Current remaining quota |
eneros_ws_connections | gauge | Current WebSocket connection count |
eneros_sse_connections | gauge | Current SSE connection count |
eneros_powerflow_running | gauge | Running power flow task count |
eneros_tenant_quota_usage_ratio | gauge | Tenant quota usage ratio (0-1) |
Recommended alerting rules:
| Alert | Condition | Severity |
|---|---|---|
| High rate limit rejection rate | rate(rejected_total[5m]) / rate(requests_total[5m]) > 0.1 | warn |
| Tenant quota about to be exhausted | quota_usage_ratio > 0.9 | warn |
| High WebSocket connection count | ws_connections > 0.8 * limit | warn |
| Power flow task queuing | powerflow_running > 0.8 * limit | warn |
Related Documentation
- REST API — Error code definitions
- WebSocket — Connection limits
- SSE Real-time Push — Connection limits
- eneros-gateway crate — Rate limiting implementation
- eneros-tenant crate — Multi-tenant quotas