Polling vs Caching vs Real-Time Updates in Web3 Dashboards
When to poll, when to cache, and when to use WebSockets for on-chain data. Trade-offs between speed, cost, and accuracy with practical examples using RPCs, The Graph, and TanStack Query.
Why live on-chain data is hard
Blockchains are append-only ledgers, not real-time databases. Every piece of data you show — balances, prices, transaction status — is a snapshot that becomes stale the moment it renders. RPC nodes have rate limits. Indexers lag behind the chain head by seconds to minutes. Price APIs throttle aggressively.
The engineering challenge is not fetching data once. It is deciding how fresh each data type needs to be, how much infrastructure cost you can afford, and what UX trade-offs your users will accept.
When to use polling
Polling is the default choice for most Web3 dashboards. It is simple, predictable, and works with any data source. Poll balances and portfolio values every 30–60 seconds. Poll transaction confirmation status every 3–5 seconds while pending. Poll protocol TVL every 60–120 seconds.
// hooks/useProtocolTVL.ts
import { useQuery } from '@tanstack/react-query';
export function useProtocolTVL() {
return useQuery({
queryKey: ['protocol-tvl'],
queryFn: async () => {
const res = await fetch('/api/tvl');
return res.json() as Promise<{ tvl: number; updatedAt: string }>;
},
staleTime: 60_000,
refetchInterval: 120_000,
refetchIntervalInBackground: false,
});
}Use refetchIntervalInBackground: false on mobile to save battery. Increase intervals during market quiet hours if your app tracks usage patterns.
When to use caching
Cache aggressively for data that changes slowly. Token metadata, contract ABIs, historical charts, and static protocol configs rarely need real-time updates.
// app/api/token-metadata/route.ts
import { unstable_cache } from 'next/cache';
const getTokenMetadata = unstable_cache(
async (address: string) => {
const res = await fetch(`https://api.etherscan.io/api?module=token&action=tokeninfo&contractaddress=${address}`);
return res.json();
},
['token-metadata'],
{ revalidate: 3600 }
);
export async function GET(request: Request) {
const address = new URL(request.url).searchParams.get('address')!;
const data = await getTokenMetadata(address);
return Response.json(data, {
headers: { 'Cache-Control': 's-maxage=3600, stale-while-revalidate=7200' },
});
}Layer caches: React Query on the client (staleTime/gcTime), Next.js unstable_cache on the server, and CDN edge caching on API routes. Each layer reduces load on the layer below.
When real-time updates matter
Use WebSockets or SSE only when sub-second latency genuinely affects user decisions: live trading interfaces, mempool monitoring, auction countdowns, or block-by-block event feeds.
// hooks/useLiveBlocks.ts — only for trading UIs
import { useEffect, useState } from 'react';
import { createPublicClient, webSocket } from 'viem';
import { mainnet } from 'viem/chains';
const wsClient = createPublicClient({
chain: mainnet,
transport: webSocket(process.env.NEXT_PUBLIC_ALCHEMY_WS_URL!),
});
export function useLiveBlocks() {
const [latestBlock, setLatestBlock] = useState<bigint | null>(null);
useEffect(() => {
const unwatch = wsClient.watchBlocks({
onBlock: (block) => setLatestBlock(block.number),
});
return () => unwatch();
}, []);
return latestBlock;
}For most dashboards, polling every 15–30 seconds delivers 95% of the UX benefit at 10% of the complexity.
Trade-offs: speed, cost, accuracy, UX
| Approach | Latency | Cost | Complexity | Best for | |----------|---------|------|------------|----------| | Polling (30s) | Medium | Low | Low | Portfolio dashboards | | Aggressive polling (3s) | Low | High (rate limits) | Low | Pending tx status | | CDN + server cache | High | Very low | Medium | Static protocol data | | The Graph | Medium | Medium | Medium | Historical events | | WebSocket RPC | Very low | High | High | Trading interfaces |
Showing stale data with a "Last updated 45s ago" timestamp is better than burning RPC credits on 1-second polling for a portfolio tracker.
Data source examples
**RPC nodes (Alchemy, Infura, QuickNode)** — direct on-chain reads. Best for balances, contract state, and pending transactions. Batch with multicall. Cache aggressively.
**The Graph** — indexed historical events via GraphQL. Best for transaction history, DEX swaps, and governance votes. Query with cursor pagination.
query GetSwaps($pool: String!, $first: Int!, $skip: Int!) {
swaps(
where: { pool: $pool }
orderBy: timestamp
orderDirection: desc
first: $first
skip: $skip
) {
id
timestamp
amount0In
amount1Out
sender
}
}**Moralis / Alchemy APIs** — higher-level endpoints for NFT metadata, token transfers, and wallet history. Good for MVPs before building custom indexers.
**DeFiLlama / CoinGecko** — price and TVL data. Cache at 30–60 second intervals. Never fetch prices per-token in a loop.
Recommended approach for user-facing dashboards
Tier your data freshness requirements:
1. **Instant (0s)** — wallet connection state, user-initiated transaction status 2. **Near-real-time (15–30s)** — token balances, portfolio values 3. **Periodic (1–5min)** — protocol TVL, market cap, historical charts 4. **Static (hours)** — token metadata, contract ABIs, documentation
Implement each tier with the cheapest strategy that meets the freshness requirement. Do not WebSocket everything.
Performance checklist
Configure staleTime per data type, not globally. Use queryKey prefixes for bulk invalidation after transactions. Prefetch data on navigation hover. Show cached data immediately while refetching in the background (isFetching indicator, not full reload). Rate-limit your own API routes to protect upstream providers. Monitor RPC usage in production — a single unbatched loop can exhaust daily limits in hours.
FAQ
**How often should I poll balances?** 30–60 seconds for display dashboards. 10–15 seconds only if users are actively trading.
**Is The Graph still relevant in 2026?** Yes for historical indexed data. For sub-second needs, consider Ponder, Envio, or direct RPC with caching.
**Should I use React Query or SWR?** TanStack Query v5. Better devtools, mutation support, and native wagmi integration.
**How do I handle data after a transaction?** Invalidate related query keys on transaction success. Refetch balances and transaction history immediately.
**What about offline support?** Show last cached data with a "Data may be outdated" banner. React Query's gcTime keeps stale data available during brief disconnections.
Want to work together? I build Web3 dashboards and DeFi interfaces.