Ometa Systems logoOmetaSystems

SaaS billing architecture mistakes that cost you revenue

The billing design errors that leak revenue and create support nightmares in SaaS products — and the architecture patterns that prevent them, from idempotency to proration to webhook reconciliation.

Published
Reading time
6 min read
Author
Ometa Systems

Billing is where SaaS products quietly lose money.

Not in dramatic, obvious ways — in the gaps. A failed renewal that nobody noticed. An upgrade that didn't take effect because a webhook was dropped. A customer double-charged because a retry fired twice. A plan change that prorated wrong and triggered a refund request. Each one is small. Together they erode revenue and trust, and they generate the support tickets nobody wants to own.

Billing looks simple in a demo — call the payments API, create a subscription, done. It is brutal in production because it's a distributed system spanning your app, a payment provider, and asynchronous events, all of which must agree on the state of the world even when one of them fails. Here are the mistakes we see most, and the patterns that prevent them.

Mistake 1: Treating the payment provider as your source of truth — or ignoring it

There are two opposite failure modes, and teams manage to hit both.

The first is storing nothing locally and querying the provider (Stripe, Paddle, etc.) on every request to learn what a customer is entitled to. This is slow, rate-limited, and fragile — when the provider is down, your app can't tell who's a paying customer.

The second is storing subscription state locally and never reconciling it with the provider, so the two drift. A card fails, the provider cancels the subscription, but your database still says "active" — and you keep serving a customer who stopped paying, or worse, the reverse.

The correct pattern is a local mirror of entitlement state, kept in sync with the provider via webhooks, with the provider as the ultimate authority. Your app reads the local copy for speed; webhooks keep it current; and a periodic reconciliation job catches anything the webhooks missed. You get fast reads and a single source of truth you can fall back to.

Mistake 2: No idempotency, so retries double-charge

Every network call can fail in the worst possible way: the operation succeeded but the response was lost. Your code, seeing no response, retries — and now you've created two subscriptions or charged the card twice.

This is not an edge case. It is a guarantee at scale. The defenses:

  • Send idempotency keys on every write to the payment provider. A stable key per logical operation means a retry returns the original result instead of creating a duplicate. Payment APIs support this precisely because the problem is universal.
  • Make your own billing endpoints idempotent. If a client double-submits a "subscribe" action, the second request should recognize the in-flight or completed operation and return the existing result, not start a new one.
  • Use database constraints as a backstop. A unique constraint on "one active subscription per customer" turns a logic bug into a caught error instead of a double charge.

Idempotency is not optional polish. It is the load-bearing wall of correct billing.

Mistake 3: Trusting webhooks to arrive exactly once, in order

Webhooks are how the payment provider tells you what happened — a payment succeeded, a subscription renewed, a card failed. Teams treat them like reliable, ordered, exactly-once messages. They are none of those things.

Webhooks can arrive late, out of order, more than once, or not at all. A "subscription updated" event can land before the "subscription created" event it depends on. The same event can be delivered twice. A delivery can simply fail and never retry into a healthy state.

Build for that reality:

  • Verify the signature on every webhook before processing. An unverified billing webhook is a security hole.
  • Process webhooks idempotently. Key on the event ID; if you've seen it, acknowledge and skip. Replays must not double-apply.
  • Don't assume ordering. Reconcile to the current state described in the event rather than blindly applying deltas. If an event says "subscription is now active until date X," set that — don't increment from whatever you had.
  • Reconcile periodically. A scheduled job that pulls the provider's view of active subscriptions and compares it to your database catches every webhook you ever missed. This single job prevents most silent revenue leaks.

Mistake 4: Getting proration and plan changes wrong

Upgrades, downgrades, and mid-cycle plan changes are where billing logic gets genuinely hard, and where homegrown math goes wrong in ways customers notice immediately.

  • An upgrade mid-cycle should usually charge the prorated difference now and adjust the next invoice. Get the proration window wrong and you over- or under-charge.
  • A downgrade is often best scheduled for the end of the current period rather than applied immediately with a refund, both for revenue and to avoid clawing back access the customer already paid for.
  • Trials, coupons, and credits interact with proration in ways that compound complexity quickly.

The lesson we apply: let the payment provider compute proration whenever possible. Providers have spent years on these edge cases. Reimplementing proration math in your app is a reliable way to generate refund requests and accounting discrepancies. Use the provider's preview/upcoming-invoice APIs to show customers exactly what they'll be charged before they confirm.

Mistake 5: No handling for failed payments (involuntary churn)

The largest preventable source of SaaS revenue loss is often not customers choosing to cancel — it's cards that expire or get declined on renewal, with no recovery flow. The customer wanted to keep paying; the system just let them lapse silently.

A proper dunning flow:

  • Listens for failed-payment events and enters a grace/retry state rather than immediately cutting off access.
  • Retries on a sensible schedule (the provider's smart retries plus your own logic) over days, not minutes.
  • Notifies the customer with a clear, easy way to update their card.
  • Defines what access looks like during grace — usually keep them in, then degrade, then suspend — so a recoverable failure doesn't become a lost customer.

Recovering even a fraction of involuntary churn often dwarfs the impact of most growth experiments.

Mistake 6: Billing logic with no audit trail

When a customer disputes a charge or finance questions a number, "the code did it" is not an answer. Billing needs to be explainable after the fact.

  • Log every billing-relevant event — the provider event, what state it changed, and when — in an append-only record.
  • Never hard-delete subscription history. Status transitions are data you will need.
  • Make the entitlement decision traceable. When a customer says "I was charged for a plan I downgraded," you should be able to reconstruct exactly what happened from the log in minutes, not spelunk through application logs hoping.

This is unglamorous infrastructure that pays for itself the first time a high-value customer disputes an invoice.

The architecture that holds up

Put together, billing that doesn't leak revenue looks like this:

  • A local entitlement mirror for fast reads, with the payment provider as the source of truth.
  • Idempotency keys on every provider write and idempotent internal billing endpoints.
  • Signed, idempotent, order-independent webhook processing, backed by a periodic reconciliation job.
  • Provider-computed proration, surfaced to the customer before they confirm.
  • A dunning flow that recovers involuntary churn instead of swallowing it.
  • An append-only audit trail that makes every charge explainable.

None of this is novel. All of it is the difference between billing as a quiet, reliable system and billing as the function that slowly leaks money and generates your hardest support tickets. Build it deliberately from day one — retrofitting correct billing onto a leaky system is far more expensive than designing it right.


We build SaaS billing systems with auth, multi-tenancy, and revenue integrity wired in from day one. Let's talk.

Continue reading

Related articles

Work with us

Building something similar?

Whether it's Postgres architecture, AI integration, or a greenfield MVP — a senior engineer will reply within one business day.

Schedule Call