Rate Limiting
Understand and handle rate limiting in AsaHome Cloud.
Overview
Rate limiting protects the API from abuse and ensures fair usage for all users. Limits are applied per-IP and per-user.
Rate Limit Tiers
| Endpoint Type | Limit | Window |
|---|---|---|
| Authentication | 5 requests | 60 seconds |
| General API | 100 requests | 60 seconds |
| WebSocket Messages | 50 messages | 10 seconds |
| Device Heartbeat | 10 requests | 60 seconds |
Rate Limit Headers
Every API response includes rate limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1700000060
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in window |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Unix timestamp when limit resets |
Rate Limit Exceeded
When you exceed the rate limit, you'll receive a 429 response:
{
"statusCode": 429,
"message": "Too many requests",
"error": "Too Many Requests",
"retryAfter": 30
}
The retryAfter field indicates how many seconds to wait before retrying.
Response Headers
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
Handling Rate Limits
Flutter Implementation
class RateLimitHandler {
int? _remaining;
DateTime? _resetTime;
void updateFromHeaders(Map<String, String> headers) {
if (headers.containsKey('x-ratelimit-remaining')) {
_remaining = int.tryParse(headers['x-ratelimit-remaining']!);
}
if (headers.containsKey('x-ratelimit-reset')) {
final timestamp = int.tryParse(headers['x-ratelimit-reset']!);
if (timestamp != null) {
_resetTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
}
}
bool get isNearLimit => (_remaining ?? 100) < 10;
Duration? get timeUntilReset {
if (_resetTime == null) return null;
final remaining = _resetTime!.difference(DateTime.now());
return remaining.isNegative ? Duration.zero : remaining;
}
}
Automatic Retry
class ApiClient {
final RateLimitHandler _rateLimitHandler = RateLimitHandler();
Future<http.Response> request(
String method,
String path, {
Map<String, dynamic>? body,
}) async {
// Check if near limit
if (_rateLimitHandler.isNearLimit) {
final waitTime = _rateLimitHandler.timeUntilReset;
if (waitTime != null && waitTime.inSeconds > 0) {
await Future.delayed(waitTime);
}
}
final response = await _executeRequest(method, path, body: body);
// Update rate limit info
_rateLimitHandler.updateFromHeaders(response.headers);
// Handle rate limit exceeded
if (response.statusCode == 429) {
final body = jsonDecode(response.body);
final retryAfter = body['retryAfter'] ?? 60;
await Future.delayed(Duration(seconds: retryAfter));
return request(method, path, body: body);
}
return response;
}
}
Exponential Backoff
Future<T> withBackoff<T>(
Future<T> Function() fn, {
int maxRetries = 3,
Duration baseDelay = const Duration(seconds: 1),
}) async {
int attempt = 0;
while (true) {
try {
return await fn();
} on RateLimitException catch (e) {
attempt++;
if (attempt >= maxRetries) {
rethrow;
}
// Use server-provided retry time or exponential backoff
final delay = e.retryAfter > 0
? Duration(seconds: e.retryAfter)
: baseDelay * pow(2, attempt);
await Future.delayed(delay);
}
}
}
Best Practices
1. Monitor Rate Limit Headers
Track remaining requests and pause before hitting limits:
void onResponse(http.Response response) {
final remaining = int.tryParse(
response.headers['x-ratelimit-remaining'] ?? '',
);
if (remaining != null && remaining < 10) {
log.warning('Approaching rate limit: $remaining remaining');
}
}
2. Batch Requests
Combine multiple requests when possible:
// Bad: Multiple requests
for (final deviceId in deviceIds) {
await getDevice(deviceId);
}
// Good: Single request
final devices = await getDevices(ids: deviceIds);
3. Cache Responses
Reduce API calls by caching:
class DeviceCache {
final Map<String, CachedDevice> _cache = {};
final Duration _ttl = Duration(minutes: 5);
Device? get(String id) {
final cached = _cache[id];
if (cached == null) return null;
if (cached.isExpired) {
_cache.remove(id);
return null;
}
return cached.device;
}
void set(String id, Device device) {
_cache[id] = CachedDevice(device, DateTime.now().add(_ttl));
}
}
class CachedDevice {
final Device device;
final DateTime expiresAt;
CachedDevice(this.device, this.expiresAt);
bool get isExpired => DateTime.now().isAfter(expiresAt);
}
4. Use WebSocket for Real-time Data
Instead of polling the API, use the WebSocket tunnel for real-time updates:
// Bad: Polling every second
Timer.periodic(Duration(seconds: 1), (_) async {
await refreshDeviceState();
});
// Good: WebSocket subscription
tunnel.subscribe(deviceUuid);
tunnel.onStateUpdate((state) {
updateDeviceState(state);
});
5. Debounce User Actions
Prevent rapid-fire requests from UI interactions:
Timer? _debounceTimer;
void onSliderChange(double value) {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 300), () {
setBrightness(value.toInt());
});
}
6. Queue Requests
For bulk operations, queue and space out requests:
class RequestQueue {
final Queue<Future<void> Function()> _queue = Queue();
final Duration _delay = Duration(milliseconds: 100);
bool _processing = false;
void add(Future<void> Function() request) {
_queue.add(request);
_processQueue();
}
Future<void> _processQueue() async {
if (_processing || _queue.isEmpty) return;
_processing = true;
while (_queue.isNotEmpty) {
final request = _queue.removeFirst();
await request();
await Future.delayed(_delay);
}
_processing = false;
}
}
WebSocket Rate Limiting
WebSocket messages are also rate-limited:
| Event | Limit | Window |
|---|---|---|
tunnel:message | 50 | 10 seconds |
tunnel:subscribe | 10 | 60 seconds |
Handling WebSocket Rate Limits
socket.on('tunnel:error', (data) {
if (data['code'] == 'RATE_LIMITED') {
final retryAfter = data['retryAfter'] ?? 10;
showSnackbar('Too many commands. Please wait $retryAfter seconds.');
// Pause sending commands
commandsPaused = true;
Future.delayed(Duration(seconds: retryAfter), () {
commandsPaused = false;
});
}
});
Per-Endpoint Limits
Some endpoints have stricter limits:
| Endpoint | Limit | Reason |
|---|---|---|
POST /auth/login | 5/min | Prevent brute force |
POST /auth/refresh | 10/min | Prevent token abuse |
POST /devices/register | 10/hour | Prevent device spam |
Monitoring Usage
Track your API usage to stay within limits:
class ApiUsageMonitor {
int _requestCount = 0;
DateTime _windowStart = DateTime.now();
void recordRequest() {
_requestCount++;
// Reset counter every minute
if (DateTime.now().difference(_windowStart).inMinutes >= 1) {
log.info('Requests in last minute: $_requestCount');
_requestCount = 0;
_windowStart = DateTime.now();
}
}
bool get isHealthy => _requestCount < 80; // 80% of limit
}
Next Steps
- Error Handling - Handle rate limit errors
- WebSocket Tunnel - Use WebSocket for real-time data
- API Reference - Explore API endpoints