Fintech technology
Startups Fintech July 18, 2026 • 12 min read

How GimBooks Scaled GST Filing to 3M Users Without a DBA

For: A fintech founder or CTO at a pre-Series A Indian SaaS company whose accounting or compliance product is growing fast but whose backend was designed for hundreds of concurrent filers, not hundreds of thousands — and is now buckling every GST deadline cycle

The bottleneck in deadline-driven compliance SaaS is almost never CPU or database throughput. It's the synchronous coupling between a user clicking "file return" and a third-party government API responding — an API you don't control, that rate-limits you, and that gets slower the closer you are to the deadline. GimBooks learned this scaling its GST filing product to millions of SMB users across India. Throwing more servers at it made the bill go up and the reliability go down. What actually worked was making the submission layer fully asynchronous, resumable, and idempotent — and treating the GSTN API as a flaky external dependency instead of an in-request call.

This post walks through what CodeNicely and the GimBooks engineering team ran into, what we tried first that didn't work, and the architectural decisions that let the product survive filing deadlines without a dedicated DBA babysitting the database at 11:47 PM on the 20th of every month.

Context: what GimBooks actually does

GimBooks is a YC-backed accounting and compliance SaaS built for Indian SMBs — kirana stores, small manufacturers, service businesses, freelancers. The core use case is invoicing plus GST return filing (GSTR-1, GSTR-3B, and related forms). Users prepare invoices through the month, then file returns before statutory deadlines — the 11th, 20th, and a handful of quarterly dates.

The traffic shape is brutal. For roughly 25 days a month, the system sees steady, predictable load. Then a 48-hour window before each deadline concentrates a huge fraction of monthly submission volume into a narrow band, and the last six hours before cutoff are worse still. SMB owners are not going to file on the 5th of the month. They file at 9 PM on the 20th, alongside everyone else, often after their CA has told them to.

You can read the full engagement summary on the GimBooks case study. This post focuses on the scaling story specifically.

The original architecture (and why it worked for 50K users)

The initial backend was a fairly standard Django monolith with PostgreSQL, Redis for caching, and a small Celery pool for background jobs. Filing a return worked like this:

  1. User clicks "File GSTR-3B."
  2. API handler validates the return, computes tax, generates the JSON payload.
  3. Same handler calls the GSTN (Goods and Services Tax Network) API to submit.
  4. Handler waits for ARN (Acknowledgement Reference Number), writes it to the DB, returns success to the user.

For a few hundred concurrent filers this is fine. The handler blocks for a few seconds, the GSTN API responds, everyone goes home. It's simple, debuggable, and the user sees an instant confirmation.

It also has three properties that make it fail catastrophically under burst load:

What we tried first (and why it didn't fix the problem)

The instinct — and the first thing most teams try — is to scale horizontally. That's what we did too. Here's what happened.

Attempt 1: More application servers

We doubled the gunicorn pool, then quadrupled it. Filing throughput improved marginally. Then Postgres started dropping connections because the connection count exploded. We added PgBouncer in transaction pooling mode. That helped for a week. Then during the March 20 filing window, response times spiked to 40+ seconds and a chunk of filings timed out with unclear state — had GSTN received them or not?

Lesson: horizontal app-tier scaling doesn't help when every request is blocked on a downstream API you don't control. You're just buying more expensive waiting rooms.

Attempt 2: Read replicas and query tuning

We assumed the DB was the problem. Added a read replica, moved reporting queries off primary, indexed everything the slow query log complained about. This was worth doing — query p99 dropped meaningfully — but it didn't move the needle on filing failures, because filing failures were never really about query time. They were about workers held open waiting for GSTN.

Attempt 3: Aggressive request timeouts

We shortened the GSTN call timeout to 5 seconds and started returning "try again" to the user on timeout. This made the dashboards look better and made the actual user experience worse. Users retried, sometimes multiple times, and we started seeing duplicate ARN attempts on the same return — a compliance nightmare, because GSTN treats duplicate submissions unpredictably.

At this point it was clear that no amount of infrastructure tuning was going to fix a design where the user's HTTP request was tied to a slow, flaky, externally-rate-limited API call.

The architectural call that actually worked

The fix was conceptually simple and operationally invasive: decouple submission from the user request entirely, and treat every filing as a durable, resumable state machine.

The new submission flow

  1. User clicks "File GSTR-3B."
  2. API handler validates the return, computes tax, generates the payload, writes a filing_job row with state = QUEUED and an idempotency key derived from (user_id, return_period, form_type). Returns to the user immediately with a job ID.
  3. User's UI moves to a "Filing in progress" state with a live status feed (server-sent events).
  4. A dedicated worker pool picks up QUEUED jobs, transitions them to SUBMITTING, calls GSTN, and transitions to ACKNOWLEDGED (with ARN) or FAILED (with a typed error).
  5. On any transient failure — network, 5xx, rate limit — the job goes back to QUEUED with an exponential backoff and a retry counter. The idempotency key prevents double-submission if a previous attempt actually reached GSTN.

This is not a novel pattern. It's the standard "outbox + worker" shape you'd use for any external side-effect. The interesting part is what it forced us to fix along the way.

Idempotency against a system that doesn't fully support it

GSTN's API surface is not designed with client-side idempotency keys the way Stripe's is. If you submit twice and the first one succeeded, you may or may not get a clean duplicate error — behavior varies by form and by time of day. So we built a two-phase check: before every submission attempt, the worker queries GSTN for the current return status. If a return for that period is already filed, we mark the job ACKNOWLEDGED using the ARN from the status response and skip resubmission. Only if the status confirms "not filed" does the worker actually submit.

This adds one extra API call per attempt. It's worth it. The cost of a duplicate filing — reconciling it, explaining it to a user's CA, potentially amending — is orders of magnitude higher than an extra status check.

Queue partitioning by deadline pressure

A naive single queue treats all jobs equally. In reality, a GSTR-1 filing due tomorrow is not the same priority as a draft save from a user filing early. We partitioned the queue by time-to-deadline, with three tiers: routine (>24h to deadline), pressured (2–24h), and critical (<2h). Each tier has its own worker pool with different concurrency and retry policies. Critical jobs get more aggressive retries and dedicated workers so a spike in routine jobs cannot starve deadline-critical ones.

Backpressure toward GSTN, not toward users

GSTN rate-limits per GSP (GST Suvidha Provider) connection. Instead of letting workers hammer the API and get throttled, we put a token-bucket rate limiter in front of the outbound call, sized to the GSP's known ceiling with a safety margin. When the bucket empties, workers wait — but the user is not waiting, because the user got a job ID and closed the tab an hour ago. Backpressure is absorbed by the queue, not by the human.

Data model changes

The original schema had one gst_return row per (user, period, form) and mutated it in place. We added an append-only filing_attempt table — every submission attempt is a row, with request payload hash, response, timestamps, and worker ID. The gst_return row now points to the current successful attempt. This gave us three things: an audit trail for regulatory disputes, the ability to replay failed submissions offline, and a clean way to distinguish "we never tried" from "we tried and GSTN said no."

What moved and what didn't

The re-architecture was not free and the metrics that improved were not the ones we initially expected.

What got dramatically better:

What got worse or stayed hard:

How we got rid of the "we need a DBA" problem

The title claim is worth defending. GimBooks does not have a full-time DBA. What replaced that role was a combination of things:

None of this is exotic. It's discipline plus the right managed services. The trap teams fall into is assuming they need a DBA when what they actually need is fewer places in the code that can generate pathological queries.

What generalizes to other compliance and fintech SaaS

If you are building a GST filing app architecture, TDS filing, e-invoicing, ROC compliance, or any product with deadline-driven bursts against a government or regulator API, the pattern is the same:

  1. Never call the regulator API from a user-facing request handler. Always queue. The user gets a job ID, not an ARN.
  2. Idempotency is your problem, not the regulator's. Assume the API has partial failures and duplicate-detection gaps. Build a pre-check + attempt log.
  3. Partition your queue by deadline pressure. All jobs are not equal in the last 2 hours.
  4. Rate-limit outbound, not inbound. Let the queue absorb bursts. Do not throttle your own users to protect the regulator — throttle your workers.
  5. Design the data model to be append-only where audit matters. Every submission attempt is evidence. Mutating in place loses information you will one day need.
  6. Instrument the leading indicators. Queue depth and worker lag tell you a deadline is going to hurt 30 minutes before your 5xx dashboard notices.

The larger lesson for fintech SaaS scaling in India: the constraint is almost always a shared external system — GSTN, NPCI, a bank's IMPS endpoint, a KYC provider, an account aggregator. Your architecture's job is to insulate your users from that system's bad days. Horizontal scaling of your own tier does not do that. Decoupling does.

How CodeNicely can help

If your compliance or accounting SaaS is buckling on deadline cycles, the fix is rarely more servers. It is usually a submission-layer redesign of the kind described above. That's the work CodeNicely did with GimBooks — the details are in the GimBooks case study — and it's the same pattern we've applied in other bursty-load environments, including the KYC and disbursal flows in CashPo and the deadline-sensitive dispatch scheduling in Vahak.

What tends to match your situation specifically: a pre-Series A team with a monolith that was correct for the first 50K users, a founding engineer who knows the system needs to be pulled apart but cannot stop shipping features to do it, and a product where a bad deadline day is a customer-trust event, not just a metrics event. We work as an embedded team — not a body-shop — take full IP handover, and stay engaged through the deadline cycles where it actually matters. More on how we work with scale-up engineering teams.

Frequently Asked Questions

Why does a GST filing app crash on deadline days but not otherwise?

Because filing traffic is bursty and synchronously coupled to the GSTN API, which itself slows down under load. Application servers hold worker processes open waiting for GSTN responses, exhausting connection pools and blocking new requests. Normal-day traffic doesn't stress this coupling; deadline-day traffic does.

Will moving to Kubernetes or a bigger database instance fix the problem?

Usually not on its own. If your submission layer is synchronously coupled to an external regulator API, adding more application capacity just means more workers waiting on the same slow downstream. You often need to decouple submission into an async, resumable job queue before infrastructure scaling produces returns.

How do you handle idempotency when the government API doesn't support client-side idempotency keys?

Two-phase check: before submitting, query the API for current filing status for that period and form. If already filed, record the existing ARN and skip. Only submit if the status confirms not-filed. Combine this with an internal idempotency key on (user, period, form) so retries do not spawn duplicate attempts.

Do we need a dedicated DBA to run a multi-tenant accounting SaaS at scale?

Not usually. What you need is a managed Postgres offering, disciplined migration and query-review practices, high-write tables moved off the transactional primary, and alerting on leading indicators like queue depth and replication lag. A DBA becomes necessary at much larger scales or when you're running self-managed clusters.

How long does it take to re-architect a synchronous filing flow into an async one?

It depends heavily on your current data model, test coverage, and how much of the submission logic is entangled with request handlers. Contact CodeNicely for a personalized assessment based on your codebase and deadline cycle.

Building something in Fintech?

CodeNicely partners with founders and tech teams to ship AI-native products that move metrics. Tell us about the problem you're solving.

Talk to our team