Why Subscription Billing Is Harder Than You Think
Every SaaS founder starts with the same assumption: billing is a solved problem. You pick a plan, charge a card monthly, and move on. In reality, the moment you introduce more than one pricing tier, annual vs. monthly options, or usage-based add-ons, you've created a combinatorial explosion of states your system needs to handle.
Consider what happens when a customer on a $49/month plan upgrades to $99/month on day 18 of a 30-day cycle. Do you charge them the full $99 immediately? Do you credit the unused portion of the $49 plan? What if they paid annually? What if they have a coupon that applies to the old plan but not the new one?
Each of these questions has multiple valid answers, and the answer you pick affects your revenue recognition, your customer's trust, and your support ticket volume. I've seen teams spend weeks debugging a billing issue that traced back to a single proration rounding decision made two years earlier.
The uncomfortable truth: Most billing bugs don't throw errors. They silently overcharge or undercharge customers by small amounts. You won't find them in your logs — you'll find them when a customer emails you a spreadsheet proving you owe them $14.37.
The Proration Problem
Proration is the process of calculating a partial charge when a subscription changes mid-billing cycle. It sounds like basic arithmetic, but the edge cases are brutal.
The core formula looks straightforward:
// Naive proration calculation
const daysRemaining = cycleDays - daysUsed;
const dailyRate = planPrice / cycleDays;
const proratedCredit = dailyRate * daysRemaining;
const proratedCharge = newPlanPrice * (daysRemaining / cycleDays);
const amountDue = proratedCharge - proratedCredit;
But here's where it falls apart. How many days are in a "month"? February has 28 (or 29). March has 31. If your billing cycle starts January 31st, what's the next billing date — February 28th? March 1st? Different billing platforms answer this differently, and the choice cascades through every proration calculation.
Cycle starts
$49/mo
Upgrade
$99/mo
Renewal
In the diagram above, the customer used 18 days of the $49 plan and needs 12 days of the $99 plan. The math: credit them $49 * (12/30) = $19.60 for unused time, charge them $99 * (12/30) = $39.60 for the new plan. Net charge: $20.00. Simple enough — until you factor in taxes, coupons, and the fact that your payment processor rounds differently than your database.
Production lesson: Always store monetary values as integers (cents), not floats. I once tracked down a $0.01 discrepancy on thousands of invoices that came from floating-point arithmetic in a proration function. The fix took 10 minutes. The reconciliation took two weeks.
Plan Change Strategies
When a customer changes plans, you have two fundamental approaches: apply the change immediately, or queue it for the end of the current billing period. Both are valid. Both have trade-offs that will shape your entire billing architecture.
Most teams I've worked with end up implementing a hybrid: immediate upgrades (because customers want their new features now) and end-of-period downgrades (because issuing mid-cycle refunds is a support nightmare). Stripe calls this proration_behavior: 'create_prorations' for upgrades and cancel_at_period_end logic for downgrades.
The trap is annual plans. A customer on a $588/year plan ($49/month equivalent) who upgrades to the $99/month tier at month 7 — do you prorate based on the annual rate or the monthly equivalent? If they paid upfront, you're sitting on 5 months of prepaid revenue that now needs to be recalculated. This is where your finance team and your engineering team will have very different opinions.
Dunning — The Art of Failed Payment Recovery
About 5-10% of recurring charges fail on the first attempt. Card expired, insufficient funds, bank flagged it as suspicious — the reasons vary, but the outcome is the same: you didn't get paid, and now you need a strategy to recover that revenue without annoying the customer into canceling.
This process is called dunning, and it's one of the highest-ROI systems you can build. A well-tuned dunning flow recovers 50-70% of initially failed payments. A bad one recovers almost nothing and churns customers who would have stayed.
The retry schedule matters more than you'd think. Retrying immediately after a failure almost never works — the same conditions that caused the decline are still present. Waiting 24-48 hours gives the customer time to replenish funds or for the bank's fraud system to cool down. Most billing platforms default to retrying on days 1, 3, 5, and 7 after the initial failure.
One thing I learned the hard way: don't cut off access the moment a payment fails. Give a grace period of 3-7 days. Customers who lose access immediately are far more likely to churn permanently than customers who get a gentle "hey, we couldn't charge your card" email while still being able to use the product. The psychology is simple — if they're still getting value, they're motivated to fix the payment issue.
Invoice Generation and Tax Complexity
Every subscription event — creation, renewal, upgrade, downgrade, cancellation — should produce an invoice. Not just for the customer's records, but for your own audit trail. When a customer disputes a charge six months from now, you need to show exactly what was billed and why.
The real complexity hits when you add tax. If you're selling to customers in multiple states (or countries), you need to calculate the correct tax rate for each invoice based on the customer's location, not yours. In the US alone, there are over 13,000 tax jurisdictions with different rates and rules about whether SaaS is even taxable.
Credit notes add another layer. When a customer downgrades or cancels mid-cycle, you don't modify the original invoice — you issue a credit note that references it. This matters for accounting compliance and for customers who need clean records for their own books. I've seen teams try to "just update the invoice amount" and end up with audit nightmares when the numbers don't reconcile against bank statements.
Practical advice: Don't build tax calculation yourself. Use a service like Avalara, TaxJar, or Stripe Tax. The cost is negligible compared to the liability of getting tax wrong in a jurisdiction you didn't even know existed. One startup I worked with discovered they owed two years of back taxes to three Canadian provinces because their homegrown tax logic only handled US states.
Five Lessons From Production
- Make every billing operation idempotent. Webhook retries, network timeouts, and duplicate API calls are not edge cases — they're Tuesday. If processing the same event twice creates a duplicate charge, you'll find out from an angry customer, not from your monitoring.
- Store the full billing state machine, not just the current state. You need to know that a subscription went from trial → active → past_due → active → canceled. Without the history, debugging billing disputes is guesswork.
- Test with time travel. Stripe's test clocks and Chargebee's time travel feature exist for a reason. Billing bugs often only surface at renewal boundaries, month-end, or leap years. If you're only testing with "create subscription, charge immediately," you're missing 80% of the failure modes.
- Separate entitlements from billing. What the customer has access to and what they're being charged for should be two different systems. When billing fails, you want a grace period where access continues. When a plan changes, you might grant access immediately but defer the charge. Coupling these tightly always ends in regret.
- Reconcile daily, not monthly. Compare your internal subscription records against your payment processor's records every single day. A one-cent discrepancy on day 3 is easy to investigate. A thousand one-cent discrepancies discovered during month-end close is a fire drill.
References
- Stripe Billing — Subscriptions Overview
- Stripe Billing — Prorations Documentation
- Chargebee — Subscriptions Documentation
- Recurly — Subscription Management Documentation
- Zuora — API Reference and Billing Documentation
Disclaimer: This article reflects the author's personal experience and opinions. Product names, logos, and brands are property of their respective owners. Pricing, features, and recovery rates mentioned are approximate and subject to change — always verify with official documentation and your own data.