Payments PendingStripe Subscription Lifecycle and Proration Handling

Stripe Subscription Lifecycle and Proration Handling

Stripe subscriptions are state machines where proration is a per-call choice, not automatic.

· 10 min read

Stripe subscriptions are not recurring payments. They are state machines with eight discrete states and defined transitions. Proration is not automatic, but a per-call policy choice. Getting both layers right is what separates a brittle integration from a reliable one.

Why the Stripe subscription lifecycle is best understood as a state machine

Most developers encounter subscriptions first as a payment that happens to recur, and the initial implementation follows from that intuition: create a subscription, store the ID, and let the charges arrive. Each transition has a defined trigger, a defined next state, and a defined business action that must follow. The developer's job is to be a faithful consumer of that machine's events, not to reimagine it from first principles.

The Subscription object carries an explicit status enum with eight named states: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, and paused. The practical consequence is that every business action, whether provisioning a workspace, adjusting entitlements, entering a dunning sequence, or revoking access, must be attached to a specific state transition. It should never be attached to a polling loop or a timer that guesses when a subscription might have changed.

The eight subscription states and the transitions that connect them

The eight states form a directed graph. Most legitimate paths lead through active, and most failure paths lead through past_due, and knowing which transitions are terminal versus recoverable determines the entire error-handling strategy for an integration. The incomplete state is entered when the initial payment attempt fails under charge_automatically. It exits to active when payment succeeds, or to incomplete_expired after 23 hours, a terminal state where the open invoice is voided and no further invoices are generated. That 23-hour boundary is not negotiable and not configurable. It is a fixed property of the graph.

The trialing state moves to active when the trial period ends. A paused subscription does not generate invoices at all, and it resumes after the customer adds a payment method. This is not the same as pausing collection, which still generates invoices but leaves the status unchanged. The canceled state is terminal unless the developer explicitly handles re-subscription, and access revocation belongs there, not in any earlier state.

What proration actually is: a per-transition billing policy, not an automatic fact

The state machine map establishes where a subscription is. The critical reframe is that proration is not something Stripe computes in the background on the developer's behalf. There is no global setting that governs it, and the default is simply one of three options that Stripe has selected, not an obviously correct answer.

The Update a subscription API accepts proration_behavior with three values. create_prorations is the default: it generates proration adjustments for billing changes but does not immediately invoice, so the customer is billed at the next renewal. none creates no prorations at all. The customer simply pays the new price from the next billing date forward. always_invoice generates prorations and immediately finalizes an invoice, triggering a payment attempt now. The API documentation's own example makes this concrete. With none, they are simply billed $200. The same transition, three different financial outcomes, and the difference is entirely the developer's choice.

Two mechanical facts constrain the decision. And usage-based, metered billing is explicitly excluded from proration mechanics, because Stripe does not prorate metered usage, which bills in arrears at period end. The consequence is that always_invoice settles only the licensed portion of a change immediately. The metered portion always settles at period end regardless of which mode is chosen.

Why upgrades and downgrades demand asymmetric proration treatment

The three-mode framework produces very different problems when applied to upgrades versus downgrades, and the correct policy is not symmetric. The default create_prorations is a reasonable starting point for upgrades but a poor default for downgrades, and applying it identically in both directions produces both customer friction and revenue distortion. For an upgrade, create_prorations credits unused time on the old plan and charges prorated time on the new plan. For B2B and enterprise contexts where billing accuracy matters more than UX smoothness, always_invoice is the better choice: it forces immediate settlement and provides an auditable trail of exactly when the change occurred and what it cost. The context, not the transition type alone, should dictate the policy.

Downgrades invert the problem. A credit from a downgrade does not flow back to the card; it reduces the next invoice. That fact must be visible in the UI before the customer confirms the change, not discovered afterward when they check their statement and see no refund.

How cancellation timing interacts with proration in ways the API makes easy to miss

Cancellation extends the asymmetric-treatment argument to a third transition, and it has a proration surface of its own that behaves differently depending on where the cancel date falls relative to the current billing period. At that point the items' current_period_end shortens to match, and in that case prorations cannot be disabled. The developer who scheduled a cancellation a quarter out has no way to prevent Stripe from prorating when the final period arrives.

The proration-date anchoring problem and the unpaid-invoice paradox

Even a correctly chosen proration mode can produce wrong results if the mechanics underneath it are not understood. Because Stripe calculates proration to the second, a preview and an update that are not anchored to the same moment will produce different amounts. The fix is to pin both the preview and the update to the same proration_date timestamp.

The second is the unpaid-invoice paradox, and it is more damaging. The correct handling is to check for open invoices before processing a mid-cycle upgrade, and either gate the change or account for the credit offset in the business logic. These are mechanical failure modes, not policy choices, and they silently corrupt the math even when the mode itself was correctly selected.

The classic vs. flexible billing mode divide and what it changes about proration

Beneath all of this sits a structural divide that determines how proration credits are actually calculated. As of API version 2025-04-30, backdating behavior differs by billing mode, and the Subscription object exposes a billing_mode field. The difference is the basis of the calculation: current configuration versus historical actuals.

The irreversibility is the critical point. This is a decision surface that must be evaluated before creating any subscription, not a setting to revisit later. If a customer holds two flexible-mode subscriptions sharing the same metered price, reported usage aggregates across both. This is a fundamental behavioral change from classic metered billing that requires re-architecting usage reporting before migration.

Subscription schedules as the principled alternative to direct-update proration

When a subscription change is planned in advance rather than immediate, subscription schedules offer an alternative to the direct-update proration pattern, and they are not an advanced or optional feature but the right default for any planned change. Schedules define changes through sequential phases, and phase transitions happen automatically when the phase's end_date is reached. Both accept the same three values.

The architectural advantage is that schedules eliminate the timing sensitivity of direct updates. The clean-invoice pattern takes this further: set proration_behavior: none on the phase transition to disable Stripe's automatic proration entirely, calculate the unused balance from the old plan, and apply it as a one-time coupon on the new phase. The customer then sees a single credit line against the new plan price, not a proration line item. A schedule can have up to 10 phases. After the last phase completes, the subscription is left in place and disassociated from the schedule unless end_behavior: cancel is set.

The billing cycle anchor and how it interacts with proration on creation

The creation-time decision that affects every subsequent proration calculation is the billing cycle anchor. It is the reference point for every future billing date, and setting it incorrectly produces misaligned proration calculations on every subsequent mid-cycle change. The anchor is a UNIX timestamp that sets the day of week for weekly intervals, the day of month for monthly and yearly intervals, and the month of year for yearly intervals. Two configuration options exist: billing_cycle_anchor_config is recommended for monthly and yearly subscriptions because it automatically handles short months and leap years, while billing_cycle_anchor accepts a timestamp directly for daily, weekly, or advanced cases.

The short-month problem illustrates why the distinction matters. Setting day_of_month: 31 creates a subscription that renews on the last day of each month, correctly handling February 28 or 29 and months with 30 days. Because the anchor is the foundation, every mid-cycle proration calculation downstream depends on it being set correctly at the start.

How pending_update and payment_behavior control what happens when a proration-triggered payment fails

Every call that sets proration_behavior to always_invoice should also set payment_behavior deliberately, because the two decisions are not separable. If you choose always_invoice without choosing a payment_behavior, Stripe applies the subscription update regardless of whether the proration payment succeeds. The resulting state is one where the subscription has been upgraded but the upgrade has not been paid for. The Update API exposes payment_behavior with four values. allow_incomplete, the default since API version 2019-03-14, applies the update and transitions to past_due if payment fails, with Stripe retrying per the account's retry settings. default_incomplete transitions to past_due without attempting payment, leaving the developer to explicitly confirm the invoice's PaymentIntent. error_if_incomplete returns HTTP 402 and does not apply the update if payment fails, though it does not support 3DS flows. pending_if_incomplete creates a pending_update that applies only if payment eventually succeeds. The pairing rule follows from this. always_invoice with pending_if_incomplete is the correct combination for upgrade-payment atomicity: the subscription does not change state until money has moved. The other modes each have their place, but none should be selected by accident, and the proration mode alone does not determine the failure behavior.

Webhook idempotency and event ordering as the practical failure surface for subscription handlers

Correct proration choices upstream are negated by non-idempotent or out-of-order webhook handling downstream. The documented failure mode is a handler processing payment_intent.succeeded to trigger provisioning that works correctly in testing but processes every payment multiple times in production because Stripe's retry logic was not accounted for. Duplicate API calls accumulate before anyone notices, and the resulting state is exactly the kind of reconstruction error the state machine model was supposed to prevent.

A second, subtler hazard is version pinning. If the account's API version is upgraded but the webhook endpoint's pinned version is not, the payload shape can diverge silently from what the handler expects, a class of failure that produces no error until a specific event type fires. If the handler is modeled as a state machine transition processor, idempotency and ordering are natural requirements, not afterthoughts.

Why testing subscription lifecycle transitions requires stateful time simulation, not static mocks

Subscription bugs hide in the sequence of state transitions over time: a trial ending, proration on a mid-cycle upgrade, payment failure triggering dunning, cancellation at period end. Testing these requires a tool that can advance time through multiple billing events and observe the resulting state, not one that returns a static response to a single API call. A test clock lets one set of associated objects move through multiple lifecycle states, which is more efficient and more accurate than creating a separate subscription for each state that needs to be tested.

The available tooling has a specific gap. It is a tool for basic sanity checks, not lifecycle simulation. Stripe's billing testing documentation adds specific hygiene: cancel subscriptions and remove related resources after each test, including when setup or assertions fail. Add run-specific metadata and schedule backup cleanup for interrupted test runs. And use mocks or fakes only when a test genuinely does not need to call Stripe.

A practical checklist for building a subscription integration that handles both layers correctly

A reliable subscription integration requires explicit decisions at seven specific points, and leaving any of them to the default without understanding its consequence is where brittle integrations begin. At creation, choose billing_mode before creating any subscription, because it cannot be reversed, and align the billing_cycle_anchor deliberately, using billing_cycle_anchor_config for monthly and yearly plans. At upgrade, choose between always_invoice and create_prorations, and if using always_invoice, pair it with payment_behavior: pending_if_incomplete to ensure payment before state change. At downgrade, prefer scheduling the change at period end rather than applying it immediately, and communicate that any credit reduces the next invoice, not the card balance. At cancellation, use cancel_at_period_end: true as the safe default for self-serve, and audit whether a custom cancel_at crosses a period boundary and whether prorations can be disabled in that case. In the webhook handler, verify signatures, deduplicate by event ID, handle the API version pin separately from the account API version, and build handlers as stateless processors that publish to an internal bus. In testing, use Test Clocks for exploratory lifecycle testing in Stripe test mode, and use a stateful simulator for CI pipelines that need offline, repeatable, multi-transition coverage without live credentials.

Sources

  1. Set the subscription billing renewal date
  2. Subscription schedules
  3. Update a subscription
  4. The Subscription object
  5. Backdate subscriptions
  6. Cancel subscriptions
  7. Pending updates
  8. Prorations

More in Stripe Integration