developer
Building a Real-Time Payment Status Polling System with WebSockets and Redis
Build an event-driven UPI payment status system using WebSockets and Redis Pub/Sub. Eliminate HTTP polling, reduce server load, and deliver instant green checkouts.
When a customer scans a dynamic UPI QR code on their laptop screen using their smartphone, they expect the browser to instantly flash green with a cheerful confirmation chime the second their UPI PIN is authenticated.
Historically, frontend engineers achieved this using client-side HTTP short polling—firing a GET /check-status request every 2 seconds. At scale, this practice crushes database connection pools and degrades server performance.
Here is the modern, event-driven blueprint using WebSockets and Redis Pub/Sub.
The Death of Client-Side HTTP Polling
Direct Answer: HTTP short-polling wastes server resources: if 5,000 customers take 60 seconds to scan and pay a QR code, your backend processes 150,000 unnecessary HTTP requests. WebSockets replace this polling storm with a single lightweight persistent TCP socket per checkout session, delivering instant push notifications with zero redundant compute.
Old Polling Method (150,000 requests / min):
Browser ──► "Paid yet?" ──► Server (No)
Browser ──► "Paid yet?" ──► Server (No)
Browser ──► "Paid yet?" ──► Server (Yes!)
Modern Event-Driven Method (1 WebSocket + 1 Push):
Browser ◄═════ Persistent WebSocket Socket ═════► Server
▲
Bank Webhook Lands ─────────────────────────────────┘
Server pushes { status: "PAID", orderId: "ORD104" } instantly!
System Architecture Overview: WebSockets + Redis
In production environments with multiple autoscaled servers behind Nginx:
[ Customer Browser ]
│
▼ (WebSocket Handshake: Room "order_ORD_9918")
[ Node.js Worker 1 ] (Maintains Client Socket)
│
▲ (Subscribes to Redis Channel: "payment_events")
│
[ Redis Cluster (Pub/Sub) ]
▲
│ (Publishes: { orderId: "ORD_9918", status: "SUCCESS" })
[ Node.js Worker 2 ] (Receives VyaparGateway Bank Webhook)
By decoupling socket connections with Redis Pub/Sub, Worker 2 can receive the webhook and broadcast it across the entire cluster, ensuring Worker 1 pushes the event to the user’s browser in under 15 milliseconds.
Backend Implementation (Node.js + Socket.io + Redis)
Install the required packages:
npm install express socket.io ioredis dotenv
Create server.js:
import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import Redis from 'ioredis';
const app = express();
app.use(express.json());
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });
// Redis Connections (One for publishing, one for subscribing)
const redisPublisher = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
const redisSubscriber = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
// 1. Subscribe to Payment Events Channel
redisSubscriber.subscribe('payment_events', (err) => {
if (err) console.error('Redis subscription failed:', err);
});
// 2. Broadcast incoming Redis events to the specific Order Room
redisSubscriber.on('message', (channel, message) => {
if (channel === 'payment_events') {
const event = JSON.parse(message);
io.to(`order_${event.orderId}`).emit('payment_update', event);
}
});
// 3. Client WebSocket Connection
io.on('connection', (socket) => {
socket.on('join_order_room', (orderId) => {
socket.join(`order_${orderId}`);
console.log(`Socket ${socket.id} joined room for Order ${orderId}`);
});
});
// 4. Inbound Webhook Endpoint from VyaparGateway
app.post('/api/webhooks/payment', async (req, res) => {
const { client_txn_id, status, amount, utr } = req.body;
if (status === 'SUCCESS') {
// Publish to Redis cluster
await redisPublisher.publish(
'payment_events',
JSON.stringify({
orderId: client_txn_id,
status: 'PAID',
amount,
utr,
})
);
}
res.status(200).json({ received: true });
});
server.listen(4000, () => console.log('Payment WebSocket Service on port 4000'));
Frontend Checkout Client Implementation
On your checkout page (React or plain HTML):
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<div id="checkout-container">
<div id="qr-box">
<img src="/api/qr/ORD_9918" alt="Scan UPI QR" />
<p>Awaiting UPI Confirmation...</p>
</div>
<div id="success-screen" style="display: none;">
<h2>Payment Successful!</h2>
<p>Redirecting to your order confirmation...</p>
</div>
</div>
<script>
const socket = io('https://api.yourstore.com');
const currentOrderId = 'ORD_9918';
// Join dedicated order room
socket.emit('join_order_room', currentOrderId);
// Listen for instant push notification
socket.on('payment_update', (data) => {
if (data.status === 'PAID') {
document.getElementById('qr-box').style.display = 'none';
document.getElementById('success-screen').style.display = 'block';
// Play celebratory chime and redirect
setTimeout(() => {
window.location.href = `/order/confirmation/${currentOrderId}`;
}, 1500);
}
});
</script>
Scaling to 100,000 Concurrent Checkouts
For multi-million user shopping events:
- Tune Linux Kernel Epoll: Increase file descriptor limits (
ulimit -n 65535) in/etc/security/limits.conf. - Use Socket.io Redis Adapter: Install
@socket.io/redis-adapterfor transparent horizontal clustering across Kubernetes pods. - Heartbeat Ping/Pong: Configure ping intervals (25s) to cleanly terminate orphaned mobile connections when users close their browser.
Explore our Developer Implementation Guides to integrate zero-commission UPI payments with modern reactive frontends.
Direct answers
Frequently asked questions
- Why is WebSocket superior to short-polling for UPI QR checkouts?
- Short-polling floods your backend with redundant HTTP requests every 2 seconds, exhausting database connections and increasing server costs. WebSockets maintain a persistent bi-directional channel that pushes a notification to the browser the exact millisecond the bank confirms payment.
- How does Redis Pub/Sub scale WebSockets across multiple server nodes?
- When multiple application servers run behind a load balancer, the server receiving the webhook might not be the same server holding the user's active WebSocket connection. Redis Pub/Sub broadcasts the payment event across all nodes so the correct client socket immediately receives the push.
- What is the recommended fallback if a client's WebSocket connection drops?
- Web applications should implement exponential backoff short-polling or Server-Sent Events (SSE) as a graceful fallback if the user's corporate firewall or mobile browser terminates the WebSocket connection.
Build your payment flow
Explore the API and browser-only merchant tools.
Create UPI checkout orders, verify signed events, or test the free calculators and generators without exposing credentials.