Subscription Billing Edge Cases
That Break Most Payment Systems

Proration, mid-cycle changes, dunning cascades, failed card recovery — here's where most billing engines fall apart and why.

Blog post  — Subscription billing state machine diagram with edge case transitions
March 12, 2026  ·  10 min read  ·  Engineering

Subscription billing looks simple on paper: charge a card on a recurring schedule, give the customer access when payment succeeds, revoke it when payment fails. The happy path is maybe 50 lines of code.

The edge cases are where billing systems earn their complexity. Most SaaS companies discover this not during implementation but in production — when a customer upgrades mid-cycle, or when a card decline triggers a sequence of events that leaves their account in an inconsistent state.

This is a writeup of the specific edge cases we see cause problems most often, what actually fails, and what a robust implementation looks like.

Proration math that doesn't round correctly

Mid-cycle plan upgrades require proration: credit the remaining days on the old plan, charge for the remaining days on the new plan. The calculation seems straightforward: divide monthly cost by days in the billing period, multiply by remaining days.

The problem is that this calculation produces irrational numbers in most billing periods. A $99/month plan with 17 days remaining in a 31-day month gives you $99 × (17/31) = $54.2903... per cycle. Round that to two decimal places and you're at $54.29.

Do this a hundred thousand times with slight variations in billing period length (28 vs 30 vs 31 day months, leap years) and you accumulate rounding discrepancies that show up in your revenue recognition reports. Not dramatically — we're talking cents per customer — but at scale this creates audit questions and reconciliation headaches.

The correct implementation uses integer arithmetic throughout. Work in cents, not dollars. Calculate proration as floor(price_in_cents * days_remaining / days_in_period) and be explicit about whether you credit by floor or ceiling. Document which rounding rule your system uses and apply it consistently. The exact rule matters less than having one and applying it uniformly.

Trial-to-paid transitions and the authorization timing problem

When a trial ends and you attempt the first paid charge, authorization timing matters. Most billing systems either: (a) wait until trial end to attempt authorization, or (b) pre-authorize the card at trial start and capture at trial end.

Option (a) creates a gap: the customer gets notification that their trial is ending, and then — sometimes hours later — you attempt authorization. If the card declines, they're in a failed state immediately. Your dunning sequence kicks in. They're annoyed. Some churn.

Option (b) (pre-auth at trial start) solves the authorization problem but creates a different one: pre-authorizations hold up to $1 on the card until capture, but they expire in 5–7 days depending on the issuing bank. A 14-day trial means your pre-auth has expired by the time you try to capture. Capturing an expired pre-auth often results in a soft decline, and your billing system may not handle "captured but pre-auth expired" correctly — some treat this as a hard decline and immediately cancel the subscription.

The robust pattern is a zero-amount authorization at trial start (many processors support this specifically for subscription validation), followed by a fresh authorization and capture at trial end with your full retry logic in place. The zero-amount auth confirms the card is valid without creating a hold that will expire. At trial end, treat it like any other subscription renewal attempt with full dunning logic, not a special code path.

Upgrade/downgrade within a billing cycle that has already invoiced

A customer upgrades from a $49/month plan to a $199/month plan. They're three days into a monthly billing cycle that already generated and paid a $49 invoice. What happens?

The clean answer: credit $49 × (27/30) = $44.10 against the new plan, generate a prorated invoice for the remainder of the current period at $199 × (27/30) = $178.90, then charge $178.90 - $44.10 = $134.80. Net result: customer pays exactly what they owe for the period.

The problem is the invoice already exists and is already paid. You're now creating a credit against a closed invoice, generating a new invoice for the current period that's partially offset by that credit, and the credit is applied before the charge rather than as a refund.

Most billing engines handle this wrong in at least one edge case: they either don't apply the credit (charging the full prorated new plan price), they generate a refund to the original payment method (which triggers bank-level reversals and fee complications), or they apply the credit but don't correctly handle the case where the credit exceeds the new invoice amount (customer downgrading creates a negative invoice).

If you're building this yourself, model credits explicitly as balance entries separate from invoices. Credits reduce the next invoice's charge amount, not the previous invoice's paid status. Negative invoice amounts should flow as balance credits, not refund triggers, unless the customer explicitly requests a refund.

Dunning logic that creates card block escalations

Dunning — the process of retrying failed payment attempts — is where most billing systems cause the most damage to their own approval rates. The pattern we see repeatedly: a card declines, the billing system retries every 24 hours for seven days, the card declines seven times in a row, the issuing bank flags the merchant as aggressive and starts blocking future charges from that merchant for that card number.

Retry frequency directly impacts your future approval rate with issuers who practice this kind of velocity blocking. Three retries in 24 hours is worse than three retries over a week. The issuer sees rapid retries as suspicious behavior — either a compromised card being tested, or a merchant that doesn't respect decline signals.

Retry timing should follow decline codes, not a fixed schedule. Soft declines (insufficient funds — code 51, temporary block — code 57) warrant a retry after 3–5 days. Hard declines (stolen card — code 62, do not honor — code 05) should not be retried at all — the card is invalid. Card-not-present declines often resolve when the cardholder updates their billing address; trigger an email to update payment method rather than retrying the current card.

The optimal dunning sequence for most SaaS companies: immediate retry once (catches transient bank errors), then day 3, day 7, day 14, with customer notification before each retry. That's four attempts over two weeks, with increasing urgency in customer communications. Beyond day 14, the probability of recovery drops below 15% and aggressive retrying does more damage to future approval rates than it recovers.

Concurrent billing operations on the same subscription

This one is subtle and causes data integrity issues rather than obvious failures. If two processes can both attempt billing operations on the same subscription concurrently — say, a scheduled renewal job and a customer-initiated plan change both running at the same time — you can end up with double charges, conflicting state updates, or both operations failing.

Subscriptions need optimistic locking or explicit state machine transitions that prevent concurrent modifications. A subscription in "renewal_in_progress" state should not be modifiable by plan change requests until the renewal completes or fails. Your billing workers and your API endpoint handlers need to be aware of each other, which usually means going through the same state machine layer rather than writing directly to the subscription record.

This isn't theoretical. We've seen customers with millions of active subscriptions hit this on monthly renewal days when hundreds of thousands of renewals run in parallel and their API layer doesn't serialize access correctly. The result: partial double-billing that's expensive to audit and reverse.

PayLoop handles these edge cases in the platform

Proration, dunning logic, state machine transitions — these are solved in the billing engine, not left for you to implement.

Get API Keys