This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Project Overview
StableRoute Frontend is the web interface for StableRoute, an open-source decentralized liquidity routing protocol built on the Stellar network. The application is built with Next.js (App Router), React, and TypeScript.
The platform provides a comprehensive suite of tools for traders, liquidity providers, and developers:
Payment and Liquidity Routing: An interactive interface for querying optimal swap and cross-currency path routing quotes across Stellar asset pools.
Liquidity Pool Management: Workflows to register, inspect, and configure supported asset pairs.
Real-Time Protocol Monitoring: A live status dashboard that polls protocol health, operational state (active vs. paused), and liquidity pool counts.
Developer and Infrastructure Controls: Management consoles for generating and revoking API keys, registering webhook event subscribers, viewing immutable system audit logs, and inspecting OpenAPI documentation.
Bug Fix or Performance Improvement
The Bug
In src/app/stats/Client.tsx, dashboard statistics were updated using a raw setInterval:useEffect(() => {
const id = setInterval(refetch, POLL_MS);
return () => clearInterval(id);
}, [refetch]);
This caused three critical issues:
- Overlapping In-Flight Requests (Race Conditions) setInterval fired strictly on a 5-second timer regardless of how long the network request took. If a request stalled or took 6 seconds due to network lag, a second request was fired before the first one finished. This created stacked concurrent requests where an older, delayed response could overwrite newer data in React state.
- Backend Hammering During Outages (Thundering Herd) When the backend experienced downtime, high load, or returned errors (500s/429s), the frontend never backed off. It continued hammering the failing endpoint every 5 seconds unabated, causing rate limits and thrashing the UI with re-rendering error alerts.
- Timer Churn on Re-render Because refetch was in the dependency array, whenever parent component updates created a new function reference, the old interval was cleared and a new one started from scratch, disrupting regular polling cadence.
How I Fixed It
I replaced setInterval with a custom, lifecycle-aware scheduling hook called useBackoffInterval:
- Settled-State Scheduling Instead of a blind clock, the hook waits for the request to settle into either 'success' or 'error' before scheduling the next timer. While a request is in the 'loading' state, no timers run, completely eliminating overlapping requests.
-
Exponential Backoff on Failures
We track consecutive failures using a ref (failureCountRef). When errors occur, the delay doubles progressively:⎛ failures ⎞delay = min ⎝baseMs × 2 ,maxMs⎠
This increases the retry delay from 5s to 10s, 20s, 40s, and caps at 60s, giving backend services breathing room to recover.
- Instant Cadence Recovery On the very first successful response (status === 'success'), the failure counter resets to 0 and the polling interval immediately returns to the standard 5-second cadence without requiring user intervention.
- Ref-Stabilized Callback We stored the callback in useRef(callback) and synced it on every render (callbackRef.current = callback). This guarantees that the timer always executes the freshest callback closure without tearing down and restarting the active timer on unrelated UI re-renders.
- Clean Unmount Teardown Each scheduled timer returns a cleanup function calling clearTimeout(timeoutId), preventing memory leaks and state updates on unmounted components.
Code
Pull Request / Branch: https://github.com/comzzy-comzzy/Stableroute-frontend/tree/performance/stats-polling-backoff
Target Repository: https://github.com/StableRoute-Org/Stableroute-frontend
Commit: 786106d62e4697aa4a9b536ab75bb320101edd9b (perf(stats): back off polling interval after repeated failures)
// Custom hook in src/app/stats/Client.tsx
export function useBackoffInterval(
status: PollStatus,
callback: () => void,
options: BackoffIntervalOptions = {}
): void {
const {
baseMs = POLL_MS, // 5,000ms
maxMs = MAX_POLL_MS, // 60,000ms
schedule = scheduleTimeout,
cancel = cancelTimeout,
} = options;
const callbackRef = useRef(callback);
const failureCountRef = useRef(0);
callbackRef.current = callback;
useEffect(() => {
// Only schedule next tick after a request settles
if (status !== 'success' && status !== 'error') return;
if (status === 'success') {
failureCountRef.current = 0;
} else {
failureCountRef.current += 1;
}
const delayMs =
status === 'success'
? baseMs
: Math.min(baseMs * 2 ** failureCountRef.current, maxMs);
const timeoutId = schedule(() => callbackRef.current(), delayMs);
return () => cancel(timeoutId);
}, [baseMs, cancel, maxMs, schedule, status]);
}
My Improvements
- Custom useBackoffInterval Hook: Instead of hacking state inside the UI component, I decoupled retry scheduling into a reusable, lifecycle-aware hook.
- Settled-State Gating: Scheduling only runs when the request status reaches 'success' or 'error'. While the status is 'idle' or 'loading', no timers are scheduled, strictly preventing concurrent in-flight requests.
- Deterministic Exponential Backoff with Upper Bound: Consecutive failures increase the delay exponentially (baseMs * 2^failures), progressing from 10s to 20s, 40s, and capping at 60s (maxMs). As soon as an update succeeds, failureCount resets to 0 and normal 5-second polling immediately resumes.
- Ref-Stabilized Callback Execution: Storing the refetch callback in a ref (callbackRef.current = callback) prevents timer teardown and re-creation when parent component props change across renders.
- Inversion of Control for Deterministic Testing: The hook accepts optional schedule and cancel functions. This allowed writing deterministic unit tests that execute synchronously without flakey time-dependent assertions.
- Comprehensive Unit and Integration Test Suite: Added comprehensive test coverage in src/app/stats/page.test.tsx verifying: .Base 5-second cadence on consecutive successful responses. .Delay doubling under network failures up to the 60-second limit. Immediate delay reset to 5 seconds upon recovery. .Prevention of duplicate alert renders during error backoff. .Clean timer cancellation on component unmount.
- Contract Documentation: Documented the behavior, parameter options, and lifecycle guarantees in docs/hooks.md.
Best Use of Google AI
Throughout the development, architecture design, debugging, and testing phases of this project and related protocol repositories, I utilized Google Antigravity paired with the Gemini 3.5 Flash (High) model as a primary engineering assistant across multiple tech stacks (React/TypeScript, Rust Soroban, Solidity, and Node.js microservices).
- Primary Project: Frontend Exponential Backoff & Lifecycle Safety (Stableroute-frontend) PR / Branch: performance/stats-polling-backoff (Commit 786106d) How Google AI Was Used: Root-Cause Diagnosis: Analyzed client network waterfall logs to isolate why client browsers flooded backend endpoints with requests during API degradation. Gemini 3.5 Flash (High) identified the unmanaged setInterval concurrency hazard. Hook Architecture: Formulated the useBackoffInterval pattern. The model advised storing active callbacks in useRef to prevent timer re-creation on renders while gating execution strictly to settled status states ('success' | 'error'). Deterministic Test Harness: Generated the Jest mock-timer test suite (src/app/stats/page.test.tsx), verifying exact progressive delay doubling (10s, 20s, 40s, 60s cap) and unmount cleanups.
Top comments (0)