Skip to main content

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 TypeLimitWindow
Authentication5 requests60 seconds
General API100 requests60 seconds
WebSocket Messages50 messages10 seconds
Device Heartbeat10 requests60 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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix 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:

EventLimitWindow
tunnel:message5010 seconds
tunnel:subscribe1060 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:

EndpointLimitReason
POST /auth/login5/minPrevent brute force
POST /auth/refresh10/minPrevent token abuse
POST /devices/register10/hourPrevent 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