Skip to main content

Handling Plan Changes

Upgrades, downgrades, and mid-period plan switches.

Upgrades, downgrades, and mid-period plan switches are where pricing complexity tends to surface. Limitr gives you the right primitives — the decisions are about when to reset meters, what to do with active grants, and how to sequence the calls correctly so your billing system stays in sync.


The core operation

setCustomerPlan(id, planId, overwrite_meters?) is the single call that changes a customer's plan. It returns true if the plan actually changed, false if the customer was already on that plan. On a successful change it fires two events: customer-set and customer-plan-changed.

const changed = await policy.setCustomerPlan('user_abc', 'growth');
// true if changed, false if already on growth
Limitr Cloud

Cloud depends on the customer-set event only, so setCustomer(customer) — which sets the entire customer object at once — also works fine for plan changes in Cloud mode.


overwrite_meters: the most important decision

The third argument to setCustomerPlan() defaults to true. This is the most consequential decision in a plan change.

ValueBehavior
true (default)Resets all meter values to zero. Customer starts fresh on the new plan.
falsePreserves current meter values. Consumption carries forward onto the new plan's entitlements.

Neither is universally correct. The right choice depends on what the plan change means in your product.

When to reset meters (true)

Upgrades where the new plan is a clean break. A customer on Starter (500K tokens/day hard limit) upgrades to Growth (2M tokens/day soft limit). They've used 450K tokens today. Resetting gives them the full 2M allocation immediately — the right experience for an upgrade. They paid for more; they should get more now.

Downgrades at period end. If you're processing a scheduled downgrade at billing period renewal, resetting meters is correct — the new period starts fresh.

Trial-to-paid conversions. A customer converting from trial to paid should start with clean meters on their first paid period.

When to preserve meters (false)

Mid-period downgrades where consumption should count. A customer downgrades from Growth to Starter mid-month. They've used 300K tokens today against Growth's 2M limit. If you reset, they'd get a fresh 500K Starter allocation immediately — effectively a free day of extra capacity as a reward for downgrading. Preserving the meter means their 300K counts against the Starter limit of 500K, leaving them 200K for the rest of the day.

Lateral plan switches. Moving a customer between plans at the same price point where you want consumption to be continuous.

When you're handling proration manually. If your billing provider is computing a proration credit, preserving meters keeps usage data coherent with what was billed.


The full upgrade sequence

async function upgradeCustomer(customerId: string, newPlan: string) {
// 1. Change the plan — reset meters on upgrade
const changed = await policy.setCustomerPlan(customerId, newPlan, true);
if (!changed) return { success: false, reason: 'Already on this plan' };

// 2. Sync included topups for the new plan
// Adds grants for topups included on the new plan.
// Removes grants from topups that were on the old plan but aren't on the new one.
await policy.ensureCustomerIncludedTopups(customerId);

// 3. Trigger the subscription charge for the new plan
// Only if your plans have a subscription entitlement set up.
// No-op if the meter is already >= 1.
await policy.ensureCustomerPlanQuantity(customerId);

// 4. Update your billing provider
// Do this after Limitr state is set — Limitr is the source of truth
// for what plan the customer is on. Don't let a billing failure leave
// Limitr and your billing provider out of sync.
await billing.updateSubscription(customerId, newPlan);

return { success: true };
}

Why this sequence

Plan first, topups second. ensureCustomerIncludedTopups() reads the customer's current plan to determine which topups are applicable. Call it after setCustomerPlan(), not before.

Subscription charge after plan change, before billing provider. ensureCustomerPlanQuantity() fires the meter-overage event your billing handler uses to queue the charge. If this fires before setCustomerPlan(), the event payload references the old plan.

Handle the false return. If the customer is already on the target plan, don't re-run the topup and subscription logic — it's a no-op at best, a double-charge at worst.


The full downgrade sequence

async function downgradeCustomer(customerId: string, newPlan: string) {
// Preserve meters on downgrade — consumption should count against the new plan
const changed = await policy.setCustomerPlan(customerId, newPlan, false);
if (!changed) return { success: false, reason: 'Already on this plan' };

// Sync topups — removes included grants no longer applicable on the new plan.
// Purchased grants (not tied to an included topup) are not removed.
await policy.ensureCustomerIncludedTopups(customerId);

// No subscription charge here — downgrades typically don't trigger a new charge.
// Handle any proration credit in your billing provider separately.

await billing.updateSubscription(customerId, newPlan);
return { success: true };
}

Grant behavior on downgrade

ensureCustomerIncludedTopups() removes grants for topups no longer on the customer's plan — but only for included topups. Grants created from purchased topups (where the customer paid explicitly) are not removed. They remain on the customer and continue to be drawn against applicable overages on the new plan if the exchange table allows it.

To remove all grants on downgrade regardless of origin, do so explicitly before calling ensureCustomerIncludedTopups().


Mid-period plan changes and meters

When a customer changes plans mid-period and you preserve meters, their current consumption may already exceed the new plan's limits. This is handled correctly:

  • If the new plan's limit is hard and the meter already exceeds it, the next allow() call returns false immediately.
  • If the new plan's limit is soft and the meter exceeds it, the customer is already in overage. The next allow() fires meter-overage.
  • If the new plan's limit is observe, nothing changes — the meter continues accumulating.

In all cases the meter state is coherent. Whether this is the right behavior for your product is a business decision, not a technical one.


Scheduled plan changes

Some products defer plan changes to the end of the billing period. Limitr doesn't have a built-in scheduler — schedule the change in your own infrastructure and call setCustomerPlan() when the period ends:

// In your billing renewal worker
async function processBillingRenewal(customerId: string) {
const account = await db.getAccount(customerId);

// Apply any pending plan change at period start
if (account.pendingPlan && account.pendingPlan !== account.currentPlan) {
await policy.setCustomerPlan(customerId, account.pendingPlan, true);
await policy.ensureCustomerIncludedTopups(customerId);
await db.clearPendingPlan(customerId);
}

// Trigger subscription charge for the new period
await policy.ensureCustomerPlanQuantity(customerId);
}

Per-customer overrides as an alternative

Sometimes you don't need to change the plan — you need to adjust a single limit for a specific customer. Enterprise deals, pilot customers, support resolutions. createCustomerOverride() changes one entitlement's limit without touching the plan or the meters:

// Give this customer 2x their plan's token limit, expiring in 30 days
await policy.createCustomerOverride(
'user_abc',
'chat_input',
4000000,
Date.now() + 30 * 24 * 60 * 60 * 1000 // expires_on
);

// Revert to plan default
await policy.removeCustomerOverride('user_abc', 'chat_input');

Overrides are the right tool when the change is customer-specific and temporary. Plan changes are the right tool when the change reflects a different product tier.

Watch for override sprawl

If you find yourself managing overrides at scale — giving everyone on a "growth-plus" arrangement an override rather than a proper plan — that's a signal you need a new plan, not more overrides.