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:

  1. Immediately, invoiced now. The customer gets the new plan and pays the prorated difference at once. Stripe calls this always_invoice.
  2. Immediately, invoiced later. The customer gets the new plan now; the prorated difference appears on the next regular invoice. Stripe's default create_prorations behaves this way.
  3. At the next renewal. No proration at all (none in 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.

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.