Subscription Proration in Practice: Month Lengths, Rounding, and Plan Changes
Proration is simple arithmetic plus policy. A lab computes one upgrade for every 2026 month and shows where 30-day and 365-day shortcuts change the invoice.
A customer on a $100 monthly plan upgrades to the $300 plan ten days into the month. What should they pay today? The textbook answer gives back two-thirds of $100 and charges two-thirds of $300, for a net of $133.33. That answer is right only in 30-day months, and only if you round the way the textbook did.
Proration is simple arithmetic wrapped in policy decisions: how long the period really is, when the change takes effect, how to round, and whether downgrades refund anything. This guide works through each decision. A lab computes the same upgrade for every month of 2026, a daylight-saving month, and a leap-year contract, so you can see how much each assumption moves the invoice.
The arithmetic
For a change at time t inside a billing period that runs from start to end:
fraction remaining = (end - t) / (end - start)
credit for old plan = old price x fraction remaining
charge for new plan = new price x fraction remaining
amount due now = charge - credit
Everything difficult is hidden in two words: what counts as the length of the period, and when t is.
Stripe, for example, calculates prorations down to the second by default and notes that a previewed amount can change slightly before the update is applied for that reason; see its prorations documentation. If you use a billing provider, its rules are the ones your customers will see, so read them before building your own preview.
Lab: one upgrade, twelve months
The program computes a $100 to $300 upgrade at 00:00 UTC on day 11 of each month in 2026, using exact rational arithmetic and rounding to cents at the end. It compares:
- second-based proration with each line rounded separately (what an invoice usually shows);
- the "every month has 30 days" shortcut, which always uses 20 of 30 days;
- rounding only the net amount once.
It then repeats the March calculation with billing anchored at local midnight in New York, where daylight-saving time starts on March 8, 2026, and computes an annual seat true-up halfway through a normal year and a leap year. Run it with go run .; it needs only the standard library and its built-in time zone database.
// Lab: how much do calendar assumptions and rounding order change a proration?
//
// Upgrade from a $100.00 plan to a $300.00 plan at 00:00 UTC on the 11th day
// of each 2026 month. Amounts use exact rational arithmetic and are rounded to
// cents half away from zero. Nothing here models a specific billing provider.
package main
import (
"fmt"
"math/big"
"time"
)
func cents(r *big.Rat) int64 { // round half away from zero to whole cents
n := new(big.Int).Set(r.Num())
d := r.Denom()
q, m := new(big.Int).QuoRem(n, d, new(big.Int))
if new(big.Int).Mul(new(big.Int).Abs(m), big.NewInt(2)).Cmp(d) >= 0 {
if n.Sign() >= 0 {
q.Add(q, big.NewInt(1))
} else {
q.Sub(q, big.NewInt(1))
}
}
return q.Int64()
}
func money(c int64) string {
sign := ""
if c < 0 {
sign, c = "-", -c
}
return fmt.Sprintf("%s$%d.%02d", sign, c/100, c%100)
}
// prorate returns credit, debit and net in cents for a change at `at`
// inside [start, end), using the given remaining/total durations.
func prorate(oldCents, newCents int64, remaining, total int64) (credit, debit, netSeparate, netExact int64) {
f := big.NewRat(remaining, total)
c := new(big.Rat).Mul(big.NewRat(oldCents, 1), f)
d := new(big.Rat).Mul(big.NewRat(newCents, 1), f)
credit, debit = cents(c), cents(d)
return credit, debit, debit - credit, cents(new(big.Rat).Sub(d, c))
}
func main() {
const oldPlan, newPlan = 10000, 30000
fmt.Println("Upgrade $100 -> $300 at 00:00 UTC on day 11 of each 2026 month")
fmt.Printf("%-4s %4s | %-31s | %-24s | %s\n", "", "days", "second-based (credit/debit/net)", "30-day assumption (net)", "net if rounded once")
for m := time.January; m <= time.December; m++ {
start := time.Date(2026, m, 1, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 1, 0)
at := start.AddDate(0, 0, 10)
rem, tot := int64(end.Sub(at)/time.Second), int64(end.Sub(start)/time.Second)
c, d, net, exact := prorate(oldPlan, newPlan, rem, tot)
days := tot / 86400
_, _, net30, _ := prorate(oldPlan, newPlan, 20, 30) // "20 of 30 days left"
flag := ""
if net != exact {
flag = " <- differs by 1 cent"
}
fmt.Printf("%-4s %4d | %8s %8s %8s | %8s (%+6s) | %8s%s\n", m.String()[:3], days,
money(c), money(d), money(net), money(net30), money(net30-net), money(exact), flag)
}
fmt.Println("\nLocal-midnight billing anchor in America/New_York, March 2026 (DST starts March 8)")
ny, err := time.LoadLocation("America/New_York")
if err != nil {
fmt.Println("tzdata unavailable:", err)
return
}
s := time.Date(2026, time.March, 1, 0, 0, 0, 0, ny)
e := s.AddDate(0, 1, 0)
at := s.AddDate(0, 0, 10)
fmt.Printf("period length: %d s (31 x 86,400 = %d s)\n", int64(e.Sub(s)/time.Second), 31*86400)
_, _, net, _ := prorate(oldPlan, newPlan, int64(e.Sub(at)/time.Second), int64(e.Sub(s)/time.Second))
_, _, netDays, _ := prorate(oldPlan, newPlan, 21, 31)
fmt.Printf("net by seconds: %s net by calendar days: %s\n", money(net), money(netDays))
fmt.Println("\nAnnual seat true-up: 20 seats x $600/year, added halfway through a contract year")
for _, y := range []int{2026, 2028} {
cs := time.Date(y, time.January, 15, 0, 0, 0, 0, time.UTC)
ce := cs.AddDate(1, 0, 0)
mid := cs.Add(ce.Sub(cs) / 2)
tot := int64(ce.Sub(cs) / time.Second)
rem := int64(ce.Sub(mid) / time.Second)
exact := cents(new(big.Rat).Mul(big.NewRat(20*60000, 1), big.NewRat(rem, tot)))
fixed := cents(new(big.Rat).Mul(big.NewRat(20*60000, 1), big.NewRat(rem, 31_536_000)))
fmt.Printf("contract from %s: year = %d s; due %s (using a fixed 31,536,000 s year: %s)\n",
cs.Format("2006-01-02"), tot, money(exact), money(fixed))
}
}
Output recorded on November 3, 2026 (Go 1.26.5, windows/amd64):
Upgrade $100 -> $300 at 00:00 UTC on day 11 of each 2026 month
days | second-based (credit/debit/net) | 30-day assumption (net) | net if rounded once
Jan 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Feb 28 | $64.29 $192.86 $128.57 | $133.33 ( $4.76) | $128.57
Mar 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Apr 30 | $66.67 $200.00 $133.33 | $133.33 ( $0.00) | $133.33
May 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Jun 30 | $66.67 $200.00 $133.33 | $133.33 ( $0.00) | $133.33
Jul 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Aug 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Sep 30 | $66.67 $200.00 $133.33 | $133.33 ( $0.00) | $133.33
Oct 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Nov 30 | $66.67 $200.00 $133.33 | $133.33 ( $0.00) | $133.33
Dec 31 | $67.74 $203.23 $135.49 | $133.33 (-$2.16) | $135.48 <- differs by 1 cent
Local-midnight billing anchor in America/New_York, March 2026 (DST starts March 8)
period length: 2674800 s (31 x 86,400 = 2678400 s)
net by seconds: $135.67 net by calendar days: $135.49
Annual seat true-up: 20 seats x $600/year, added halfway through a contract year
contract from 2026-01-15: year = 31536000 s; due $6000.00 (using a fixed 31,536,000 s year: $6000.00)
contract from 2028-01-15: year = 31622400 s; due $6000.00 (using a fixed 31,536,000 s year: $6016.44)
What the output shows
The 30-day shortcut is wrong in eight of twelve months. It overcharged by $4.76 in February and undercharged by $2.16 in each 31-day month. Summed over thousands of upgrades, that is a real number, and a February customer who checks the math will be right.
Rounding order changes the total by a cent. Rounding the credit ($67.74) and the charge ($203.23) separately gives a net of $135.49; rounding the exact net gives $135.48. Neither is wrong, but the invoice must add up. If you show two line items, the total has to be their sum, so round the lines and add them.
Local-time anchors change period length. A March period that starts and ends at midnight New York time is one hour short of 31 days, because the clocks jump forward. Prorating by seconds over that period gave $135.67 rather than $135.49. "Use UTC" avoids this only if your period boundaries are defined in UTC; if your contracts say "midnight in the customer's time zone", the seconds are what they are.
Leap years matter for annual contracts. Halfway through a contract year that includes February 29, 2028, dividing by a fixed 31,536,000-second year overcharged a 20-seat true-up by $16.44.
Decisions to write down before you code
| Decision | Common options | What to check |
|---|---|---|
| Unit of time | Seconds, or whole days | Whether customers can predict the amount from their invoice dates |
| Period length | Actual length of this period | Never a fixed 30 days or 365 days |
| Rounding | Round each line to the currency's minor unit, total is their sum | Currencies with zero or three decimal places |
| When an upgrade takes effect | Immediately, at payment, or at next renewal | What the customer gets if payment fails |
| Downgrades | Immediately with credit, or at period end | Whether credits become refunds or account balance |
| Who decides | Your code, or the billing provider | Show customers the provider's preview, not your own estimate |
Upgrades: decide when access changes
There are three common choices, each reasonable:
- Immediately, invoiced now. The customer gets the new plan and pays the prorated difference at once. Stripe calls this
always_invoice. - Immediately, invoiced later. The customer gets the new plan now; the prorated difference appears on the next regular invoice. Stripe's default
create_prorationsbehaves this way. - At the next renewal. No proration at all (
nonein Stripe); the new price starts with the next period.
If access changes immediately but payment can still fail, decide what happens then: do you keep the upgrade, revert it, or start the dunning process? Whatever you choose, apply the entitlement change idempotently, because the event that confirms payment may arrive twice or late; the webhook guide shows how.
Downgrades: credit now or change later
Downgrading immediately gives the customer a credit for the unused part of the higher plan. Many products instead schedule downgrades for the end of the paid period. That avoids removing features the customer has already paid for, avoids issuing small credits, and keeps the customer on the higher plan until the date they expect.
Neither is universally better. Immediate downgrades suit usage-sensitive products where the higher plan carries real cost for you. Scheduled downgrades suit feature tiers. Show the effective date and the financial effect on the confirmation screen, and store the scheduled change as data, so support can see it and cancel it.
Seats added in the middle of a period
For per-seat pricing, each added seat is a small proration. Charging each one immediately creates many small card payments, which customers find noisy. Collecting seat changes and invoicing them together at the next renewal or once a threshold is reached produces fewer, larger invoices at the cost of extending credit for a few weeks.
For annual contracts, the usual approach is co-terming: added seats are prorated to the existing renewal date, so every seat renews together. Compute the fraction from the actual seconds or days remaining in that contract year, which the lab shows can differ from a 365-day assumption by enough to notice.
Invoices are not revenue recognition
Proration decides what you invoice. It does not decide when you recognize revenue. Under US GAAP (ASC 606) and IFRS 15, revenue is recognized as performance obligations are satisfied, which for many subscriptions means over the service period, but the right treatment depends on your contracts and should be set by your accountants. The IFRS 15 standard and the AICPA's revenue recognition resources are the starting points for that conversation. Give finance clean data: the period each charge covers, the plan, and whether it was a proration.
Several currencies
Prorate in the currency the subscription is billed in, using that currency's minor unit, and do not convert between currencies inside a proration. If a customer moves to a plan priced in another currency, treat it as ending one subscription and starting another, with any credit handled under a written policy.
Testing your proration code
Build a table test with the cases the lab used: a 28-day month, a 31-day month, a change exactly at the period start and end, a DST transition if you anchor in local time, February 29, and a currency with no minor unit. Compare each result with your billing provider's preview for the same subscription. If the two disagree, your preview is wrong, because the provider's number is the one on the invoice.
Where to start
Find where your code computes a proration preview and check two things: whether it ever uses a constant 30 or 365, and whether its total is the sum of the rounded lines it displays. If you use a billing provider, replace your own math with the provider's upcoming-invoice preview. The unit economics calculator can then show whether the new plan prices still cover each tenant's cost.
Frequently Asked Questions
Should proration use seconds or days?
Either can be fair if it is consistent and predictable. Seconds match what providers such as Stripe do by default; whole days are easier for customers to check by hand. Whichever you choose, divide by the real length of that period.
Why does my preview differ from the final invoice by a few cents?
Second-based proration changes as time passes, so a preview shown a minute before confirmation can differ slightly. Show the provider's preview at confirmation time and explain that the amount is calculated at the moment of the change.
Should downgrades give refunds?
That is a business decision, usually written into your terms. Common choices are scheduling the downgrade for the end of the paid period, or crediting the unused amount to the account balance rather than refunding to the card.
How do I prorate quantity changes on annual plans?
Prorate each added seat from the change date to the existing renewal date using the actual length of that contract year, and invoice the total. Keep the renewal date fixed so every seat renews together.
Does proration affect revenue recognition?
It produces invoice lines, which finance then accounts for under ASC 606 or IFRS 15 according to your contracts. Provide the service period each charge covers so accountants can apply the right treatment.
Sources
Documents cited in this guide. Each link was opened and its title compared with the label below on the date shown. A source supports the specific statements it is linked from; design recommendations and lab results are Velynoriva's own.
-
Prorations
(Stripe)
• Link checked 2026-11-03
Default second-level proration and the create_prorations, always_invoice, and none behaviors.
-
IFRS 15 Revenue from Contracts with Customers
(IFRS Foundation)
• Link checked 2026-11-03
The international revenue recognition standard.
-
Revenue recognition resources
(AICPA & CIMA)
• Link checked 2026-11-03
Practitioner resources on applying ASC 606.
Revision history
- — Rewrote the worked example around a reproducible lab covering every 2026 month, rounding order, daylight saving, and a leap-year contract. Removed TeX formulas that displayed as raw text, the fixed 31,536,000-second year, the conflation of GAAP and IFRS, and universal rules for downgrades and currency conversion.
- — Clarified activation and downgrade effective dates.