Why polling doesn't scale (and how to implement streaming instead)
Learn to replace $60K/month polling with 98% cheaper DexPaprika streaming for 10,000 concurrent users—complete with production code, migration strategy, and troubleshooting guide.

Why polling doesn't scale (and how to implement streaming instead)
Polling APIs every second seems reasonable for real-time crypto prices. At 50 users, it works perfectly. But at 10,000 users, polling costs $60,000/month and creates connection storms that crash your infrastructure.
This tutorial shows how to build a production-ready streaming architecture using DexPaprika's free SSE endpoints. You'll replace expensive polling with persistent connections that cost 98% less.
What you will learn
In this tutorial, you will:
- Calculate the real infrastructure cost of polling at scale
- Identify the three hard limits that kill polling architectures
- Implement SSE streaming with DexPaprika's free endpoints
- Build production-ready connection management with reconnection logic
- Migrate from polling to streaming without downtime
- Handle 10,000 concurrent users for under $1,200/month
Prerequisites
To follow this tutorial, you need:
- Node.js 18+ installed
- Basic understanding of JavaScript async/await
- Familiarity with the EventSource API (or willingness to learn)
- A basic web server setup (Express, Fastify, or similar)
No API key required. DexPaprika streaming is completely free with no rate limits.
Quick start: your first real-time price stream
Get streaming crypto prices in 30 seconds:
// stream-quickstart.js - Copy and run immediately
const EventSource = require('eventsource');
// Connect to WETH price stream (no API key needed)
const stream = new EventSource(
'https://streaming.dexpaprika.com/stream?method=t_p&chain=ethereum&address=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
);
stream.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(`WETH Price: $${data.price} (24h: ${data.price_change_24h}%)`);
};
stream.onerror = (error) => {
console.error('Stream error:', error);
};
// Keep connection open
console.log('Connected to DexPaprika stream...');Run it:
npm install eventsource node stream-quickstart.js
You should see live WETH prices updating automatically. The browser handles reconnection if the connection drops.
Understanding why polling fails at scale
Before building the streaming solution, let's calculate exactly where polling breaks down.
The cost explosion
Polling costs scale with users × update frequency. That multiplication destroys budgets at scale.
Setup: 10,000 concurrent users, 1-second polling for crypto prices.
Requests per month:
10,000 users × 1 req/sec × 60 sec × 60 min × 24 hr × 30 days = 25.9 billion requests
AWS API Gateway cost:
First 333M requests: $3.50 per million = $1,165 Next 667M requests: $2.80 per million = $1,868 Next 19B requests: $2.38 per million = $45,220 Remaining 6B requests: $1.51 per million = $9,060 Total: $57,313 per month (just for API Gateway!)
Add bandwidth ($1,170/month for 13 TB), database costs ($1,460/month for read replicas), and application servers ($500/month). Total monthly cost: $60,443 for 10K users with 1-second polling.
The three hard limits
Costs hurt, but hard limits kill you first.
Limit 1: Browser connection cap
- Browsers allow 6 concurrent HTTP/1.1 connections per domain
- Open 7 tabs? Tab 7 hangs forever
- At 1-second polling: maximum ~40 requests/sec per browser
Limit 2: Rate limiting
- External APIs (Binance, Coinbase) have strict rate limits
- Binance: 1,200 requests/minute per IP = 20 req/sec
- For 10,000 users polling every second, you'd need 500 different IP addresses
Limit 3: Thundering herd on deploys
- All servers restart during deployment
- 10,000 clients reconnect simultaneously
- Load spike crashes servers, triggers runaway auto-scaling
- I've seen $2,000 AWS bills from a single 2 AM deploy
Streaming fixes all three problems. Let's build it.
Building your streaming infrastructure
Start with a single-token stream, then scale to multiple tokens with production-grade error handling.
Step 1: Single token price stream
Create a basic price tracker that connects to DexPaprika and displays live prices.
// price-tracker.js - Single token streaming
const EventSource = require('eventsource');
class PriceTracker {
constructor(chain, address, symbol) {
this.chain = chain;
this.address = address;
this.symbol = symbol;
this.stream = null;
}
start() {
// DexPaprika streaming endpoint (completely free)
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${this.chain}&address=${this.address}`;
this.stream = new EventSource(url);
this.stream.onopen = () => {
console.log(`✓ Connected to ${this.symbol} stream`);
};
this.stream.onmessage = (event) => {
const data = JSON.parse(event.data);
this.displayPrice(data);
};
this.stream.onerror = (error) => {
console.error(`✗ Error on ${this.symbol}:`, error);
};
}
displayPrice(data) {
const changeIndicator = data.price_change_24h > 0 ? '↑' : '↓';
console.log(
`${this.symbol}: $${data.price} ${changeIndicator} ${Math.abs(data.price_change_24h)}% (24h)`
);
}
stop() {
if (this.stream) {
this.stream.close();
console.log(`Disconnected from ${this.symbol}`);
}
}
}
// Track WETH prices
const tracker = new PriceTracker(
'ethereum',
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'WETH'
);
tracker.start();
// Graceful shutdown
process.on('SIGINT', () => {
tracker.stop();
process.exit();
});This code connects to DexPaprika's streaming endpoint for WETH. The EventSource API automatically handles reconnection with exponential backoff.
What's happening:
- EventSource creates a persistent HTTP connection
- Server pushes price updates as they occur
- Browser parses the SSE format automatically
- Connection drops? Browser reconnects with Last-Event-ID
Cost for 10,000 users: ~$800/month (vs $60,443/month with polling).
Step 2: Multi-token dashboard
Scale to track multiple tokens simultaneously with a clean management interface.
// multi-token-dashboard.js - Track multiple tokens
const EventSource = require('eventsource');
class DexPaprikaDashboard {
constructor() {
this.streams = new Map();
this.prices = new Map();
}
// Add a token to track
addToken(chain, address, symbol) {
const key = `${chain}:${address}`;
// Prevent duplicate connections
if (this.streams.has(key)) {
console.log(`Already tracking ${symbol}`);
return;
}
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${chain}&address=${address}`;
const stream = new EventSource(url);
stream.onopen = () => {
console.log(`✓ ${symbol} connected`);
};
stream.onmessage = (event) => {
const data = JSON.parse(event.data);
this.prices.set(symbol, data);
this.updateDisplay();
};
stream.onerror = () => {
console.error(`✗ ${symbol} connection failed`);
};
this.streams.set(key, { stream, symbol });
}
// Remove a token stream
removeToken(chain, address) {
const key = `${chain}:${address}`;
const entry = this.streams.get(key);
if (entry) {
entry.stream.close();
this.streams.delete(key);
this.prices.delete(entry.symbol);
console.log(`Removed ${entry.symbol}`);
}
}
updateDisplay() {
console.clear();
console.log('=== DexPaprika Live Dashboard ===\n');
this.prices.forEach((data, symbol) => {
const arrow = data.price_change_24h > 0 ? '↑' : '↓';
const color = data.price_change_24h > 0 ? '\x1b[32m' : '\x1b[31m';
const reset = '\x1b[0m';
console.log(
`${symbol.padEnd(8)} $${data.price.toFixed(6).padStart(12)} ` +
`${color}${arrow} ${Math.abs(data.price_change_24h).toFixed(2)}%${reset}`
);
});
console.log(`\n${this.streams.size} active streams`);
}
shutdown() {
console.log('\nShutting down all streams...');
this.streams.forEach(({ stream, symbol }) => {
stream.close();
console.log(`✓ Closed ${symbol}`);
});
}
}
// Usage: Track major stablecoins and WETH
const dashboard = new DexPaprikaDashboard();
// Real Ethereum mainnet token addresses
dashboard.addToken('ethereum', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 'WETH');
dashboard.addToken('ethereum', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 'USDC');
dashboard.addToken('ethereum', '0xdac17f958d2ee523a2206206994597c13d831ec7', 'USDT');
// Graceful shutdown
process.on('SIGINT', () => {
dashboard.shutdown();
process.exit();
});Run this to see a live-updating dashboard of multiple token prices. Each token has its own persistent connection.
Step 3: Production-ready connection manager
Add robust error handling, exponential backoff, and connection health monitoring.
// production-stream-manager.js - Production-grade streaming
const EventSource = require('eventsource');
class ProductionStreamManager {
constructor(options = {}) {
this.baseUrl = 'https://streaming.dexpaprika.com/stream';
this.streams = new Map();
// Configuration
this.reconnectDelay = options.reconnectDelay || 1000;
this.maxReconnects = options.maxReconnects || 10;
this.healthCheckInterval = options.healthCheckInterval || 30000;
// Monitoring
this.metrics = {
totalConnections: 0,
activeConnections: 0,
reconnects: 0,
errors: 0
};
this.startHealthCheck();
}
subscribe(chain, address, symbol, callback) {
const key = `${chain}:${address}`;
let reconnectAttempts = 0;
let reconnectTimer = null;
const connect = () => {
const url = `${this.baseUrl}?method=t_p&chain=${chain}&address=${address}`;
const stream = new EventSource(url);
let lastMessageTime = Date.now();
stream.onopen = () => {
console.log(`✓ ${symbol} connected`);
reconnectAttempts = 0;
this.metrics.totalConnections++;
this.metrics.activeConnections++;
};
stream.onmessage = (event) => {
lastMessageTime = Date.now();
try {
const data = JSON.parse(event.data);
// Validate data structure
if (!data.price || !data.symbol) {
throw new Error('Invalid data structure');
}
callback(null, data);
} catch (error) {
this.metrics.errors++;
callback(error, null);
}
};
stream.onerror = (error) => {
console.error(`✗ ${symbol} error:`, error.message);
this.metrics.errors++;
this.metrics.activeConnections--;
stream.close();
// Exponential backoff reconnection
if (reconnectAttempts < this.maxReconnects) {
reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, reconnectAttempts - 1),
30000
);
// Add random jitter (prevents thundering herd)
const jitter = Math.random() * 1000;
console.log(`Reconnecting ${symbol} in ${delay + jitter}ms (attempt ${reconnectAttempts}/${this.maxReconnects})`);
reconnectTimer = setTimeout(() => {
this.metrics.reconnects++;
connect();
}, delay + jitter);
} else {
console.error(`✗ ${symbol} max reconnects reached, giving up`);
callback(new Error('Max reconnects exceeded'), null);
this.streams.delete(key);
}
};
// Store stream and metadata
this.streams.set(key, {
stream,
symbol,
reconnectAttempts,
lastMessageTime,
reconnectTimer
});
};
connect();
}
unsubscribe(chain, address) {
const key = `${chain}:${address}`;
const entry = this.streams.get(key);
if (entry) {
// Clear reconnect timer if pending
if (entry.reconnectTimer) {
clearTimeout(entry.reconnectTimer);
}
entry.stream.close();
this.streams.delete(key);
this.metrics.activeConnections--;
console.log(`Unsubscribed from ${entry.symbol}`);
}
}
// Health check: detect stale connections
startHealthCheck() {
setInterval(() => {
const now = Date.now();
const staleThreshold = 60000; // 60 seconds
this.streams.forEach((entry, key) => {
const timeSinceLastMessage = now - entry.lastMessageTime;
if (timeSinceLastMessage > staleThreshold) {
console.warn(`⚠ ${entry.symbol} stale (${Math.round(timeSinceLastMessage / 1000)}s), reconnecting...`);
// Force reconnection
entry.stream.close();
}
});
}, this.healthCheckInterval);
}
getMetrics() {
return {
...this.metrics,
activeStreams: this.streams.size
};
}
shutdown() {
console.log('\n=== Shutdown initiated ===');
this.streams.forEach((entry) => {
if (entry.reconnectTimer) {
clearTimeout(entry.reconnectTimer);
}
entry.stream.close();
});
console.log(`Closed ${this.streams.size} streams`);
console.log('Metrics:', this.getMetrics());
this.streams.clear();
}
}This production implementation includes:
- Exponential backoff: Prevents connection storms during outages
- Random jitter: Spreads reconnection load over time
- Health checks: Detects and fixes stale connections
- Metrics: Track connection health and performance
- Graceful shutdown: Clean cleanup on process termination
Migrating from polling to streaming
Don't flip a switch. Roll out streaming gradually while keeping polling as a fallback.
Week 1-2: Deploy streaming alongside polling
Run both systems in parallel with a feature flag:
// hybrid-migration.js - Gradual rollout pattern
const EventSource = require('eventsource');
class HybridPriceFeed {
constructor(userId) {
this.userId = userId;
this.useStreaming = this.shouldUseStreaming(userId);
this.pollingInterval = null;
}
shouldUseStreaming(userId) {
// Gradual rollout: 10% of users
return userId % 100 < 10;
}
start(chain, address, callback) {
if (this.useStreaming) {
console.log(`User ${this.userId}: Using streaming`);
this.startStreaming(chain, address, callback);
} else {
console.log(`User ${this.userId}: Using polling`);
this.startPolling(chain, address, callback);
}
}
startStreaming(chain, address, callback) {
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${chain}&address=${address}`;
this.stream = new EventSource(url);
this.stream.onmessage = (event) => {
const data = JSON.parse(event.data);
callback(data);
};
this.stream.onerror = () => {
console.warn('Streaming failed, falling back to polling');
this.stream.close();
this.startPolling(chain, address, callback);
};
}
startPolling(chain, address, callback) {
const pollUrl = `https://api.dexpaprika.com/v1/token/${chain}/${address}`;
this.pollingInterval = setInterval(async () => {
try {
const response = await fetch(pollUrl);
const data = await response.json();
callback(data);
} catch (error) {
console.error('Polling error:', error);
}
}, 5000); // 5-second polling (slower than before)
}
stop() {
if (this.stream) {
this.stream.close();
}
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
}
}Monitor error rates and connection stability for a week. Fix issues before expanding.
Week 3: Increase to 50% of users
Bump the feature flag threshold:
shouldUseStreaming(userId) {
// Increase to 50% of users
return userId % 100 < 50;
}Watch infrastructure costs drop. Handle edge cases like corporate firewalls blocking SSE (fall back to polling temporarily).
Week 4-5: Full migration with polling fallback
Move all users to streaming but keep polling code as insurance:
// resilient-feed.js - Streaming-first with fallback
class ResilientPriceFeed {
constructor() {
this.failureCount = 0;
this.maxFailures = 3;
this.strategy = 'streaming'; // or 'polling'
}
connect(chain, address, callback) {
try {
this.connectStreaming(chain, address, callback);
} catch (error) {
console.error('Streaming initialization failed:', error);
this.fallbackToPolling(chain, address, callback);
}
}
connectStreaming(chain, address, callback) {
const url = `https://streaming.dexpaprika.com/stream?method=t_p&chain=${chain}&address=${address}`;
this.stream = new EventSource(url);
this.stream.onmessage = (event) => {
this.failureCount = 0; // Reset on success
const data = JSON.parse(event.data);
callback(data);
};
this.stream.onerror = () => {
this.failureCount++;
console.error(`Stream error ${this.failureCount}/${this.maxFailures}`);
if (this.failureCount >= this.maxFailures) {
console.warn('Max streaming failures reached, switching to polling');
this.stream.close();
this.fallbackToPolling(chain, address, callback);
}
};
}
fallbackToPolling(chain, address, callback) {
this.strategy = 'polling';
console.log('Using polling fallback (slower updates)');
const pollUrl = `https://api.dexpaprika.com/v1/token/${chain}/${address}`;
this.pollingInterval = setInterval(async () => {
try {
const response = await fetch(pollUrl);
const data = await response.json();
callback(data);
} catch (error) {
console.error('Polling also failed:', error);
}
}, 10000); // 10-second polling (conservative)
}
disconnect() {
if (this.stream) {
this.stream.close();
}
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
}
}After streaming proves stable for 30 days, you can remove polling code. But many teams keep it permanently as insurance during outages.
Testing your implementation
Verify your streaming implementation works correctly with these tests.
Test 1: Basic connection
node production-stream-manager.js
Expected output:
✓ WETH connected ✓ USDC connected ✓ USDT connected WETH: $2345.67 USDC: $1.0001 USDT: $0.9999
If you see connection messages and price updates, your implementation works.
Test 2: Reconnection logic
Simulate network interruption by killing your internet connection for 10 seconds. The stream manager should automatically reconnect with exponential backoff.
Expected behavior:
✗ WETH error: Connection closed Reconnecting WETH in 1234ms (attempt 1/10) ✓ WETH connected
Test 3: Stale connection detection
The health check should detect connections that haven't received data in 60 seconds and trigger reconnection.
Watch for:
⚠ USDC stale (62s), reconnecting... ✓ USDC connected
Test 4: Load testing
Simulate 100 concurrent streams to verify your system handles load:
// load-test.js
const ProductionStreamManager = require('./production-stream-manager');
const manager = new ProductionStreamManager();
// Subscribe to same token 100 times (simulates 100 users)
for (let i = 0; i < 100; i++) {
manager.subscribe(
'ethereum',
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
`WETH-${i}`,
(error, data) => {
if (error) return;
if (i === 0) console.log(`Price update: $${data.price}`);
}
);
}
// Check metrics after 60 seconds
setTimeout(() => {
console.log(manager.getMetrics());
}, 60000);Monitor memory usage and connection stability.
Troubleshooting common issues
Connection drops immediately
Symptom: Stream connects then closes within 1 second.
Check:
- Verify endpoint URL format:
https://streaming.dexpaprika.com/stream?method=t_p&chain=ethereum&address=0x... - Ensure token address is valid and checksummed
- Test network connectivity:
curl https://streaming.dexpaprika.com/
Fix: Validate the token address against Etherscan or the blockchain explorer for your chain.
No data received
Symptom: Connection stays open but no messages arrive.
Possible causes:
- Token has very low trading volume (updates only when trades happen)
- Wrong chain specified (token exists on different network)
- EventSource parser error (check for JSON parse exceptions)
Fix: Test with high-volume tokens first (WETH, USDC, USDT).
Memory leak with multiple streams
Symptom: Memory usage grows over time with many streams.
Cause: Event listeners not cleaned up properly.
Fix: Always close streams in your cleanup code:
process.on('SIGINT', () => {
manager.shutdown(); // Closes all streams
process.exit();
});Corporate firewall blocks SSE
Symptom: Connections fail from corporate networks but work from home.
Cause: Some firewalls block long-lived HTTP connections.
Fix: Implement polling fallback for affected users:
stream.onerror = () => {
// Detect corporate firewall and fall back
if (errorCount > 3) {
this.fallbackToPolling();
}
};Cost comparison: real numbers
Here's what infrastructure costs look like for a typical deployment.
Polling (baseline)
10,000 concurrent users, 1-second polling:
- API Gateway: $57,313/month
- Bandwidth: $1,170/month
- Database: $1,460/month
- Servers: $500/month
- Total: $60,443/month
- Per-user cost: $6.04/month
Streaming with DexPaprika
10,000 concurrent users, SSE streaming:
- CloudFront + Lambda@Edge: $800/month
- Bandwidth: $400/month (smaller payloads)
- DexPaprika streaming: $0/month
- Database: $200/month (95% less load)
- Servers: $100/month (connection management only)
- Total: $1,500/month
- Per-user cost: $0.15/month
Savings: $58,943/month (97.5% reduction)
For a startup burning $60K/month on infrastructure, this migration can save the company from running out of runway.
When polling still makes sense
Don't cargo cult streaming. Polling works fine in these scenarios:
Low update frequency (> 30 seconds):
1,000 users × 1 req/min × 43,200 min/month = 43.2M requests/month Cost: ~$150/month vs SSE infrastructure: ~$200/month setup + maintenance
Under 1,000 users with 60-second updates? Polling is simpler and cheaper.
One-off requests:
User clicks "refresh prices" manually? Use polling. Streaming makes no sense for user-triggered actions.
Internal tools:
Building a dashboard for your 20-person team? Polling is fine. Don't over-engineer for 5 concurrent users.
Testing and development:
Polling is easier to debug. Start with polling to prove your concept, then migrate to streaming when scale demands it (typically around 500-1,000 concurrent users).
Production deployment checklist
Before deploying streaming to production, verify:
- Health checks detect and fix stale connections
- Exponential backoff prevents thundering herd during reconnection
- Random jitter spreads reconnection load over time
- Metrics track connection health (active streams, reconnects, errors)
- Graceful shutdown closes all connections cleanly
- Polling fallback activates after 3 consecutive streaming failures
- Load testing confirms system handles your target user count
- Monitoring alerts fire when connection failure rate exceeds 5%
Next steps
You have successfully implemented production-ready streaming with DexPaprika. To extend this system:
- Add database persistence: Store price history for charts and analytics
- Implement data transformation: Calculate moving averages, volume metrics
- Scale to thousands of tokens: Use connection pooling and sharding strategies
- Build a web dashboard: Create a React/Vue frontend with live updates
- Add trading signals: Detect price movements and send notifications
Related resources
- DexPaprika API Documentation - Complete API reference
- Server-Sent Events Specification - SSE protocol details
- EventSource API on MDN - Browser API reference
Summary
Polling costs $60K/month for 10K users. DexPaprika streaming costs $1.5K/month for the same load—a 97.5% reduction. Start with the quick start example, build production features gradually, and migrate from polling using feature flags. The free streaming tier makes real-time crypto data accessible to every developer.
Related articles
Coinpaprika education
Discover practical guides, definitions, and deep dives to grow your crypto knowledge.
Cryptocurrencies are highly volatile and involve significant risk. You may lose part or all of your investment.
All information on Coinpaprika is provided for informational purposes only and does not constitute financial or investment advice. Always conduct your own research (DYOR) and consult a qualified financial advisor before making investment decisions.
Coinpaprika is not liable for any losses resulting from the use of this information.