India's UPI handled 24.51 billion payments in August 2026, according to NPCI's figures. That is about 791 million a day. Averaged over the whole day, even 3 am, it is roughly 9,000 payments every second. In the spring of 2025, UPI went down three times. The cause was not the payments. It was apps asking if their payments had gone through.
I want to walk through how a payment switch like UPI is built. It is one of the clearest examples I know of a system where speed is not the hard part. The hard part is staying correct when something in the middle stops answering.
The shape of the system
A UPI payment has four kinds of parties. It helps to name them first.
The payer's app, say PhonePe or Google Pay, is the screen you tap. It never holds your money. It reaches the network through a sponsor bank, called a PSP (payment service provider) bank. In the middle sits one central switch. NPCI, the National Payments Corporation of India, runs it. Your own bank is the remitter bank. Only it may check your PIN and take money out of your account. The payee's bank, the beneficiary bank, puts the money in.
A push payment goes like this:
- Your app sends a pay request to the switch, through its PSP bank.
- The switch asks the payee's PSP which bank account the payee's UPI ID belongs to.
- You type your UPI PIN into a secure part of the phone. It encrypts the PIN so only your bank can read it. The app and the switch carry it, but cannot read it.
- The switch asks your bank to check the PIN and take the money out.
- The switch asks the payee's bank to put the money in.
- Both apps are told the result.
Here is the part that surprises people. At this point, no money has moved between the two banks. Each bank has only changed its own customers' balances. The switch adds up what the banks owe each other. That is settled later, in batches, through their accounts at the RBI. Say one bank's customers paid another bank's customers ₹1,000 crore today, and got ₹950 crore back. Only the ₹50 crore difference moves.
So UPI has two layers. Messages move in real time. Money moves later, in batches. That split is what lets UPI feel instant. It does not have to move real money hundreds of millions of times a day.
A timeout is not a failure
Now the interesting part. The switch asks your bank to take the money out, and it works. Then it sends the credit to the payee's bank, and the request times out.
What happened? Maybe the payee's bank never got the request. Maybe it paid the payee and the reply got lost on the way back. The switch cannot tell.
Say it treats the timeout as a failure and refunds you. If the credit actually went through, the payee now has money nobody paid for. Now say it treats the timeout as a success. If the credit never happened, you have lost money.
So the only correct move is a third state: unknown. The payment stays pending. A status check or the daily matching finds out what really happened. Only then is the payment marked final or reversed.
This is the sentence I want to hear from a candidate in a payments interview. A timeout means "I do not know yet". It never means "it failed".
The RBI backs this up from the customer's side. Its 2019 circular sets the time limits. Say your account was debited, but the payee was not credited. Your bank must return the money by the next day. If it is late, it pays you ₹100 for every extra day.
Retries across banks you do not control
Networks retry. A PSP that does not hear back will send the same request again. If each retry were a new payment, one tap could take your money twice.
The fix is a transaction ID. It is made once, at the very start. Every party treats it as the name of the payment. Say a bank gets a debit request for a transaction it has already debited. It returns the earlier result. It does not take the money again.
This only works if the check and the debit are one database statement. The transaction ID must be a unique key. I tested this on PostgreSQL. A retry changes nothing. Now say two copies of the same request race. The second one hits the unique key. Its whole statement rolls back, balance update included. The account is debited exactly once:
WITH d AS (
UPDATE accounts SET balance_paise = balance_paise - $3
WHERE account_id = $2 AND balance_paise >= $3
AND NOT EXISTS (SELECT 1 FROM debits WHERE txn_id = $1)
RETURNING account_id)
INSERT INTO debits (txn_id, account_id, amount_paise)
SELECT $1, account_id, $3 FROM d;
-- 0 rows: already debited, or not enough money.
Notice the amounts are whole numbers, in paise. Never use floating point for money.
The 2025 outages: when the clients take down the server
In March and April 2025, UPI had three outages. MediaNama reported NPCI's finding. Banks and apps were calling the "check transaction status" API very often, in large numbers. They did it even for payments that had already finished.
Think about what that means. A payment is slow to confirm. The app asks, "Did it go through?" There is no quick answer, so it asks again. And again. Every one of those questions is a real request into the same central system. That system is already struggling. The slower it gets, the more everyone asks. And the more they ask, the slower it gets. That is a retry storm. It can take down a system that has plenty of room for its real job.
NPCI's fix is a short lesson in protecting a shared system:
- The first status check waits 45 to 90 seconds after the payment.
- Each payment gets at most three status checks, within two hours.
- No retries on connection errors, like "connection timeout".
- Some APIs get rate limits.
From August 2025, it also moved other work that is not urgent away from the busy hours. Each user can check their balance at most 50 times a day in each app. Each user can ask for their linked accounts at most 25 times a day. Autopay debits run only outside the busiest hours. Those are 10 am to 1 pm and 5 pm to 9:30 pm.
None of these stop anyone from paying. They move everything that is not a payment away from the moments when payments need the room.
Removing a feature to remove an attack
UPI first let anyone send a collect request. It says, "Please pay me ₹X", and you approve it with your PIN. For shops, that is useful. Between strangers, it became a tool for fraud. Requests were dressed up as refunds or prizes. Since 2019, they had been capped at ₹2,000. From 1 October 2025, NPCI switched off collect requests between people completely. Only merchants can still use them.
I like this one because it has nothing to do with scale. A feature can work exactly as designed and still be the easiest way to steal from your users. The safest payment is the one the payer starts.
What I would say in an interview
Start with the four parties and one push payment, end to end. Then spend most of your time on what breaks:
- A timeout on the credit step is unknown, not failed. Keep it pending until the daily matching says otherwise.
- The transaction ID is the idempotency key at every bank. It is written in the same transaction as the money.
- Money settles later, in net batches. The money moves based on the switch's records, so they must be perfect.
- The central switch must protect itself from its own clients. Give each caller a rate limit. Give "what happened?" questions their own path. Make clients wait longer between retries instead of hammering.
I wrote up the full design in the UPI system design walkthrough. It has the data model, the parts, the trade-offs and a source for every number. To see the same ideas from inside one payment app, read the PhonePe design next. And idempotency keys goes deeper on the retry problem above.


Top comments (0)