Reduce task polling frequency and raise rate limits to prevent 429s

With 8+ active tasks, polling every 5s generates ~96 task_status
requests/min, exceeding the 60/60s rate limit. Two fixes:

- Adaptive polling: 30s when WebSocket is connected (safety net),
  5s only when WebSocket is down (primary source)
- Raise task_status rate limit to 200/60s and tasks_for_user to
  60/60s to handle burst scenarios (page reloads, WS reconnects)
This commit is contained in:
Viktor Barzin 2026-02-09 22:59:39 +00:00
parent 791b5a9d55
commit 3616e678ac
No known key found for this signature in database
GPG key ID: 0EB088298288D958
2 changed files with 21 additions and 10 deletions

View file

@ -27,8 +27,8 @@ class RateLimitConfig:
"/api/listing_geojson": EndpointLimit(10, 60),
"/api/listing_geojson/stream": EndpointLimit(10, 60),
"/api/refresh_listings": EndpointLimit(3, 300),
"/api/task_status": EndpointLimit(60, 60),
"/api/tasks_for_user": EndpointLimit(30, 60),
"/api/task_status": EndpointLimit(200, 60),
"/api/tasks_for_user": EndpointLimit(60, 60),
"/api/cancel_task": EndpointLimit(10, 60),
"/api/clear_all_tasks": EndpointLimit(5, 60),
"/api/get_districts": EndpointLimit(20, 60),
@ -87,8 +87,8 @@ class RateLimitConfig:
"/api/listing_geojson": _parse_limit("RATE_LIMIT_GEOJSON", 10, 60),
"/api/listing_geojson/stream": _parse_limit("RATE_LIMIT_GEOJSON_STREAM", 10, 60),
"/api/refresh_listings": _parse_limit("RATE_LIMIT_REFRESH", 3, 300),
"/api/task_status": _parse_limit("RATE_LIMIT_TASK_STATUS", 60, 60),
"/api/tasks_for_user": _parse_limit("RATE_LIMIT_TASKS_FOR_USER", 30, 60),
"/api/task_status": _parse_limit("RATE_LIMIT_TASK_STATUS", 200, 60),
"/api/tasks_for_user": _parse_limit("RATE_LIMIT_TASKS_FOR_USER", 60, 60),
"/api/cancel_task": _parse_limit("RATE_LIMIT_CANCEL_TASK", 10, 60),
"/api/clear_all_tasks": _parse_limit("RATE_LIMIT_CLEAR_TASKS", 5, 60),
"/api/get_districts": _parse_limit("RATE_LIMIT_DISTRICTS", 20, 60),

View file

@ -1,11 +1,13 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AuthUser } from '@/auth/types';
import type { TaskState, TaskStatusResponse, WSMessage } from '@/types';
import { WS_TASKS_PATH, POLLING_INTERVALS } from '@/constants';
import { WS_TASKS_PATH } from '@/constants';
import { fetchTasksForUser, fetchTaskStatus } from '@/services';
const KEEPALIVE_MS = 30_000;
const MAX_RECONNECT_DELAY_MS = 30_000;
const POLL_FAST_MS = 5_000; // WS down — polling is the primary update source
const POLL_SLOW_MS = 30_000; // WS up — polling is just a safety net
function wsUrl(token: string): string {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
@ -116,9 +118,15 @@ export function useTaskProgress(user: AuthUser | null): UseTaskProgressReturn {
}
}, []);
const startPolling = useCallback(() => {
const startPolling = useCallback((intervalMs: number = POLL_SLOW_MS) => {
const currentUser = userRef.current;
if (!currentUser || pollingTimer.current) return;
if (!currentUser) return;
// Stop existing polling before (re)starting with the requested interval
if (pollingTimer.current) {
clearInterval(pollingTimer.current);
pollingTimer.current = null;
}
const fetchAndPoll = async () => {
const u = userRef.current;
@ -157,7 +165,7 @@ export function useTaskProgress(user: AuthUser | null): UseTaskProgressReturn {
};
fetchAndPoll();
pollingTimer.current = setInterval(fetchAndPoll, POLLING_INTERVALS.TASK_STATUS_MS);
pollingTimer.current = setInterval(fetchAndPoll, intervalMs);
}, []); // No reactive deps — reads from refs
// ---- WebSocket connection ----
@ -173,6 +181,9 @@ export function useTaskProgress(user: AuthUser | null): UseTaskProgressReturn {
setIsConnected(true);
reconnectAttempt.current = 0;
// WS is primary — slow down polling to safety-net rate
startPolling(POLL_SLOW_MS);
// Start keepalive pings
keepaliveTimer.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
@ -219,8 +230,8 @@ export function useTaskProgress(user: AuthUser | null): UseTaskProgressReturn {
keepaliveTimer.current = null;
}
// Fallback to polling while WS is down
startPolling();
// WS is down — poll at fast rate as primary update source
startPolling(POLL_FAST_MS);
// Exponential backoff reconnect
const delay = Math.min(