The Tech Behind 100ms Bidding: A Deep Dive into Pusher & Redis
When 500 people are watching a live auction and someone places a bid, every single screen needs to update instantly. Not in one second. Not in 500 milliseconds. We're talking sub-100ms from bid click to every connected client seeing the new amount.
Here's how we built it.
The Problem With HTTP Polling
The naive approach to real-time updates is polling — hitting an API endpoint every second to check for changes. This fails spectacularly for auctions:
- 500 users polling every second = 500 requests/second = your server is toast
- 1-second latency means two people can bid the same amount simultaneously
- Wasted bandwidth — 99% of polls return "nothing changed"
We needed true real-time. That means WebSockets.
Architecture Overview
Our bidding pipeline has three layers:
Client (Browser) → API Server → Redis → Pusher → All Clients
Each layer is optimized for a specific job:
- API Server validates the bid (auth, purse check, minimum increment)
- Redis handles atomic state updates and pub/sub
- Pusher broadcasts the update to all connected clients
Redis: The Single Source of Truth
Every auction's state lives in Redis, not in a traditional database. Here's why:
- Atomic operations —
INCR,SETNX, and Lua scripts prevent race conditions - Sub-millisecond reads — checking current bid takes < 1ms
- TTL-based expiry — auction state auto-cleans after the event ends
When a bid comes in, the server runs a Lua script that atomically:
- Checks if the bid exceeds current highest bid + minimum increment
- Checks if the team has sufficient remaining purse
- Updates the current bid, bidder, and timestamp
- Publishes the update to a Redis channel
All four steps happen in a single atomic operation. No race conditions possible.
Pusher: Broadcasting at Scale
Once Redis confirms a valid bid, we need to tell every connected client. Pusher handles this via WebSocket channels:
- Each auction gets a dedicated channel (
auction-{id}) - Clients subscribe when they open the auction page
- Pusher handles reconnection, fallback to long-polling, and connection multiplexing
The key insight: we don't broadcast from our server. We trigger a Pusher event, and Pusher's global infrastructure handles delivery to all clients. This means our server load doesn't scale with viewer count.
The Client Side
On the frontend, bid updates arrive as WebSocket events. The update flow:
- Pusher event received with new bid data
- React state updates via a dedicated auction store
- UI re-renders with the new bid amount, team name, and timer reset
- A subtle animation draws attention to the change
We use requestAnimationFrame to batch UI updates, preventing jank when multiple bids arrive in quick succession (common in heated bidding wars).
Handling Edge Cases
Real-time systems have gnarly edge cases. Here's how we handle the worst ones:
Simultaneous Bids
Two teams bid at the exact same millisecond. Redis Lua scripts handle this — only one bid can win the atomic compare-and-set. The loser gets an immediate "outbid" response.
Network Disconnections
If a team owner loses connectivity mid-bid:
- Their bid in flight either committed to Redis or it didn't — no partial state
- On reconnect, the client fetches current state via HTTP as a fallback
- Pusher's client library handles automatic reconnection with exponential backoff
Auctioneer Controls
The auctioneer (host) has special powers: start/pause/sold/unsold. These events go through the same Redis → Pusher pipeline but on a separate presence channel that also tracks who's online.
Performance Numbers
In production, across hundreds of live auctions:
| Metric | Value | |--------|-------| | Bid-to-screen latency (p50) | 47ms | | Bid-to-screen latency (p99) | 94ms | | Max concurrent viewers (single auction) | 2,400+ | | Redis operation time | < 2ms | | Pusher delivery time | 30-60ms | | Server processing time | 8-15ms |
The bottleneck is almost always Pusher's delivery time (network latency to the client), not our server.
Why Not Build Our Own WebSocket Server?
We considered it. Here's why we chose Pusher instead:
- Global edge network — Pusher has servers worldwide, reducing latency for users in different regions
- Automatic scaling — we don't manage WebSocket server capacity
- Presence channels — built-in tracking of who's online
- Reliability — Pusher handles reconnection, message ordering, and delivery guarantees
The trade-off is cost, but the engineering time saved is worth it for our scale.
What's Next
We're exploring WebTransport (the successor to WebSockets) for even lower latency, and experimenting with edge computing to run bid validation closer to users. Stay tuned.
Want to experience sub-100ms bidding for yourself? Start a free auction and see the tech in action.