SaaS Billing Proration Mathematics on Stripe Custom Workflows
A mathematical analysis of custom proration, trial-anchoring, and automated customer win-back pipelines.
Why Default Proration Silently Leaks Trust
Mid-cycle plan changes are where billing trust is won or lost. Stripe's default behavior is economically correct but UX-blind: it quietly books proration line items onto the customer's next invoice, and the customer sees a charge they can't explain. Support tickets follow. Churn follows the tickets.
The fix is not to avoid proration — it is to run the math yourself first, preview it, and present it before you commit it. This post walks the exact arithmetic Stripe runs on every mid-cycle change, the three behavior modes that control when money moves, how trial anchoring changes the picture, and the win-back pipeline for customers who already left.
---
The Proration Formula Stripe Actually Runs
Every mid-cycle change reduces to two fractions of the billing period:
$\text{credit} = P_{old} \times Q_{old} \times \frac{t_{unused}}{T} \qquad \text{charge} = P_{new} \times Q_{new} \times \frac{t_{remaining}}{T}$
where T is the length of the current billing period, t_unused is the paid-but-now-unused portion of the old price, and t_remaining is the portion of the period still to be served at the new price. Stripe books these as two proration invoice items — a credit and a charge — and the customer's net adjustment is:
$\text{net} = \text{charge} - \text{credit}$
Worked example. A customer pays $50/mo for Pro and upgrades to $120/mo Business on day 20 of a 30-day cycle:
One precision detail that matters in custom workflows: Stripe computes prorations from exact timestamps, to the second — not whole days. The day-based arithmetic above is for readable math; your preview should pass real Unix timestamps.
That timestamps-not-days detail is also what makes proration_date powerful. You can back-date the calculation to the moment the customer clicked the upgrade button rather than the moment your worker processed it — so a webhook queue delay of a few minutes never changes what the customer is charged.
---
proration_behavior: Three Modes, Three Revenue Stories
Every subscription and subscription-item update takes the same three-way switch:
create_prorations (default) — Stripe generates the credit/charge invoice items as pending items. They roll onto the next regular invoice. Right math, no surprise mid-cycle charge. Use it when upgrades are allowed freely and you consolidate billing.always_invoice — Stripe generates the prorations and immediately finalizes an invoice for them. This is the "upgrade now, pay the difference now" mode. Use it when the new plan unlocks features instantly and the product promise is pay-now-upgrade-now.none — No credit, no charge. The customer gets the rest of the period at the old price, the new price starts next cycle. This is a deliberate business decision — a free upgrade window, a promo, or a migration where adjustment lines would confuse more than they explain.The mode is not an implementation detail — it is pricing policy. Deciding it per plan tier, not per codebase, is what keeps a hundred-tenant billing surface coherent.
---
Preview Before You Commit
The pattern that eliminates billing surprises entirely: show the customer the exact proration before it exists. Stripe's upcoming-invoice preview accepts the same parameters you intend to apply and returns the invoice that would result — proration lines included — without persisting anything.
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// 1. Preview — nothing is persisted, nothing is charged
const prorationTs = Math.floor(Date.now() / 1000);
const preview = await stripe.invoices.retrieveUpcoming({
customer,
subscription: subId,
subscription_items: [{ id: itemId, price: process.env.PRICE_BUSINESS_ID! }],
proration_date: prorationTs,
proration_behavior: "always_invoice",
});
// 2. Show preview.amount_due to the customer in the UI:
// "Upgrade now for a one-time $23.33, then $120/mo from Sep 1."
// 3. On confirmation, apply with the SAME parameters
await stripe.subscriptions.update(subId, {
items: [{ id: itemId, price: process.env.PRICE_BUSINESS_ID! }],
proration_behavior: "always_invoice",
proration_date: prorationTs,
});
The iron rule of custom proration workflows: preview parameters and apply parameters must be identical, including the timestamp. If the preview says $23.33 and the application charges $24.10 because you re-derived proration_date seconds later, you have rebuilt the trust problem you were engineering away.
---
Trial Anchoring: The Anchor Is Everything
A trial is anchored by trial_end, and proration only ever applies to paid time — which changes the math completely during a trial:
trial_end untouched) generates no proration at all. There is no paid time to credit; the change simply alters what billing begins at trial_end. The customer sees nothing mid-trial.trial_end = now, optionally billing_cycle_anchor: "now") starts a billing period immediately. Pair it with always_invoice to charge the new period right away, and the preview flow above becomes a "skip your trial, start today" upsell with the amount shown up front.billing_cycle_anchor: "now") starts a fresh full period now — which would double-charge the days already paid in the old cycle. Stripe's docs recommend proration_behavior: "create_prorations" precisely to credit that overlap. Anchor reset without the credit is a refund ticket generator.One 2025+ wrinkle worth knowing: Stripe's flexible billing mode removed several implicit anchor resets that older behavior had (operations like setting cancel_at or changing prices could silently move your anchor). If your integration predates it, audit for implicit resets — silent anchor drift makes every proration calculation downstream wrong.
---
The Win-Back Pipeline
Churned revenue has structure, and Stripe gives you three windows to work it — each with different proration math:
Window 1 — Dunning (payment failed, subscription past due). Smart Retries re-attempt failed payments on ML-optimized schedules, and dunning emails give the customer a self-serve path to fix the card. Every recovery here costs nothing in proration — the subscription never changed. This is the cheapest revenue you will ever recover.
Window 2 — The cancellation window. When a customer cancels, prefer cancel_at_period_end: true over immediate cancellation. The subscription stays active until the period ends, and the door stays open:
// Customer asked to cancel on day 12, then changes their mind on day 19 —
// before the period ends. Continuity preserved: same subscription,
// same billing anchor, zero proration lines, zero re-onboarding.
await stripe.subscriptions.update(subId, { cancel_at_period_end: false });
This is the highest-converting win-back moment you will ever have: no new checkout, no new subscription object, no math at all. Current API versions also expose an explicit resume for this in-window case — but cancel_at_period_end: false remains the dependable cross-version pattern.
Window 3 — Terminal cancellation. Once the period ends, the subscription is canceled — a terminal state. Reactivation means creating a new subscription, and here the anchor math returns:
// Win-back offer: rejoin now, full price starts on the 1st.
await stripe.subscriptions.create({
customer,
items: [{ price: process.env.PRICE_PRO_ID! }],
billing_cycle_anchor: anchorForFirstOfNextMonth(),
proration_behavior: "none", // the partial period until the anchor is free
});
The free partial period costs you a few days of service; the alternative is a pro-rated first invoice that explains itself badly to a customer you are trying to re-earn. In win-back flows, none is not lost revenue — it is CAC you already paid for.
---
The Commercial Value of Proration Correctness
Proration is one of the few places where correct math and customer trust are the same thing. The arithmetic is fixed — credit the unused, charge the remaining, on exact timestamps — but the workflow around it is custom: preview before commit, identical parameters on both calls, anchor discipline through trials and resets, and a win-back pipeline that knows which window a lapsed customer is standing in.
Engineer those four pieces once and billing stops being a support category. Customers see the math before it charges them, upgrades feel instant without feeling sneaky, and canceled customers come back through a door you left deliberately open.