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.

VT VyaparGateway Team Payments & Compliance 2 min read
Building a Real-Time Payment Status Polling System with WebSockets and Redis guide
real time upi payment status websocket redis pub sub payment notification upi qr status check without polling real time system design VyaparGateway

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-adapter for 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.