Back to BlogTroubleshooting

Fintech app cannot handle transaction volume: scaling the money path

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

When a fintech app buckles under transaction volume, the cause is rarely raw traffic. It is a hot row every transaction updates, a synchronous call to the gateway blocking a request, or an unindexed query over a growing ledger. Find the real bottleneck first. Then scale the money path with care, because the one thing you must never trade for speed is counting money correctly.

If you would rather have the money path scaled for you, see how RAITHub would build it at the end of this guide.

Why does my fintech app slow down or lock up under load?

Because somewhere on the money path, requests are waiting on the same resource instead of running in parallel. The symptom is slowness or timeouts; the cause is almost always one of a few patterns. Diagnose the pattern before you add servers, or you will pay for capacity that cannot be used.

SymptomLikely bottleneckWhy it limits you
Writes queue up; one account is slowA hot row: a shared balance or counter every transaction locksRow locks serialise writes; concurrency collapses to one at a time
Requests time out under trafficA synchronous call to the gateway inside the requestYour throughput is capped by the slowest external call
Reports and dashboards crawlUnindexed queries over a growing ledgerA sequential scan gets slower every day the table grows
Connections exhaustedNo connection pooling, or long transactions holding connectionsThe database runs out of connections before it runs out of CPU
Everything slow at peak onlyWork done in-request that belongs in a background jobPeaks exceed what synchronous processing can absorb

How do I find the real bottleneck on the money path?

Measure, do not guess. The money path has a few predictable choke points, and each leaves a signature you can look for.

  1. Look at lock waits. In PostgreSQL, query pg_stat_activity and pg_blocking_pids() during a slow spell. If writes are blocked on a lock, you have a hot row, not a traffic problem.
  2. Find the slow queries. Enable the slow-query log, or inspect pg_stat_statements, and run EXPLAIN (ANALYZE, BUFFERS) on the worst. A sequential scan over the ledger is a missing index.
  3. Time the external calls. Instrument the gateway call. If a charge takes most of the request time, that call does not belong inside the request.
  4. Watch the connection count. If it pins at the limit while CPU is idle, you need pooling, not bigger machines.
  5. Load-test the money path specifically, not the marketing pages. Replay a realistic mix of charges, refunds and reads at the volume you expect, and watch what fails first.

This is the same discipline as any performance incident, applied to payments. The general version is in recovering from a broken migration, which shows the pg_blocking_pids query for finding locks.

How do I scale the money path without double-counting?

Scaling a fintech app is different from scaling a content app, because correctness cannot bend. A dropped page view is nothing; a dropped or doubled ledger entry is money. The rules below buy throughput while keeping the count exact.

TechniqueWhat it buysThe correctness guardrail
Move charges to a queueRequests return fast; load smooths over peaksJobs must be idempotent so a retry cannot double-charge
Append-only ledger entriesInserts scale far better than row updatesDerive balances from entries; never update a shared balance row in place
Index the query patternsReports and lookups stay fast as the table growsIndex on account and date; keep write amplification in mind
Connection poolingMany app instances share a safe number of DB connectionsKeep transactions short so connections free up
Partition the ledger by timeOld data stays out of hot queriesTotals must still roll up across partitions correctly

The biggest single win is usually replacing a hot balance row with append-only entries. Instead of every transaction updating one row (which serialises all of them), you insert immutable entries and compute the balance from them, with periodic snapshots for speed. Modern Treasury's engineering write-up on how to scale a ledger discusses this class of problem. The append-only design itself is in double-entry ledger database design.

What does moving the charge off the request path look like?

Record the intent synchronously, enqueue the charge, and process it in a worker that is safe to retry. The sketch is TypeScript.

// Request path: fast, durable, returns immediately.
async function startPayment(order: Order): Promise<{ paymentId: string }> {
  const payment = await db.payment.create({
    orderId: order.id,
    status: 'pending',
    idempotencyKey: crypto.randomUUID(), // reused on every retry of this job
    amountMinor: order.amountMinor,
  })
  await queue.enqueue('charge', { paymentId: payment.id })
  return { paymentId: payment.id } // customer is not kept waiting on the gateway
}

// Worker: idempotent, so a crash-and-retry never charges twice.
async function processCharge({ paymentId }: { paymentId: string }) {
  const payment = await db.payment.findById(paymentId)
  if (payment.status === 'captured') return // already done; safe no-op
  const result = await gateway.charge(payment, { idempotencyKey: payment.idempotencyKey })
  await db.payment.markCaptured(paymentId, result.txnId) // UNIQUE(txnId) guards replays
}

The queue smooths peaks and frees the request, but it only stays correct because the job is idempotent and the database rejects duplicate captures. The mechanics are in stopping duplicate payments and idempotency in API design.

Buy, build or hire?

OptionExampleChoose this whenWhere it stops
Vertical scalingA bigger database and app instancesYou are nowhere near a hot-row limit yetA hot row does not get faster with more CPU
Managed ledger productModern Treasury or a ledger engine such as TigerBeetleYou want a scalable ledger without building oneAnother system to integrate and operate
Rearchitect in-houseAppend-only ledger, queues, indexes, poolingYour bottleneck is specific and you have the teamYou own the correctness tests and the migration
Hire an engineering teamDiagnose, then rebuild the money pathYou are losing throughput now and cannot find whyNot a substitute for capacity planning with finance

How long does it take to fix yourself?

Our estimate: one to three days to find the bottleneck with the right tools and add the missing indexes or pooling; two to six weeks to move charges to a queue, convert a hot balance to an append-only ledger, and prove it under load. The main risk of doing it alone is trading correctness for speed: a change that lets the ledger double-count under concurrency is far worse than the slowness it fixed.

How RAITHub would build this

  • Diagnose: measure lock waits, slow queries, external-call time and connection use to name the real bottleneck.
  • Off the request path: move charges to idempotent background jobs so requests return fast and peaks smooth out.
  • Scale the ledger: append-only entries with derived balances and snapshots, indexed for the real query patterns.
  • Prove it: load tests on the money path and concurrency tests that assert no double-count.

Timeline: 6–12 weeks as a backend engagement, depending on how much of the ledger and queueing has to change; a focused fix to indexes and pooling is shorter. What you receive: automated tests and CI, load-test fixtures, handover docs, and full IP under NDA. Production and financial data stay in your own cloud account; development uses synthetic data.

RAITHub is an engineering studio and has not shipped a regulated or licensed fintech product; this is general information, so confirm regulatory and capacity questions with your advisers. Proof we can point to: PadhAI, built by RAITHub, ran 11 services with a payment abstraction over 9 gateways, and Sundor Skin enforces money rules in PostgreSQL across 146 tables under 530+ automated tests. Next step: a free 15-minute technical audit, then a written fixed quote. Book the audit. More is on the SaaS development page, the FinTech page, and in our payments engineering guide.

Frequently asked questions

Why does my fintech app slow down under load but the servers look idle?

Usually a hot row or exhausted connections. If every transaction locks the same balance row, writes serialise and concurrency collapses regardless of CPU. If connections pin at the limit while CPU is idle, you need pooling and shorter transactions, not bigger machines.

How do I find the bottleneck on the money path?

Measure, do not guess. Check lock waits in pg_stat_activity, find slow queries with pg_stat_statements and EXPLAIN ANALYZE, time the gateway call, and load-test the money path specifically with a realistic mix of charges, refunds and reads.

Should I move charges to a background queue?

Often yes, because it frees the request and smooths peaks. But only if the job is idempotent and the database rejects duplicate captures, otherwise a retried job can charge twice. Record the intent first, enqueue the charge, and make the worker a safe no-op when the payment is already captured.

Why is an append-only ledger easier to scale?

Because inserts scale far better than contended updates to a shared balance row. You compute balances from the immutable entries, with periodic snapshots for speed, instead of serialising every transaction on one row.

Can I just buy a bigger database?

It helps until you hit a structural limit like a hot row, which does not get faster with more CPU. Diagnose the bottleneck first; vertical scaling buys time but does not fix a serialisation problem.

fintech scalingtransaction volumedatabase performanceledgerqueuesPostgreSQL

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.