Skip to content

Billing Pitfalls And Troubleshooting

Billing Pitfalls & Troubleshooting

This document covers common mistakes, subtle bugs, and edge cases in the billing system.

Race Conditions

TOCTOU: Check-Then-Act Without Lock

Pitfall: Check if payment is duplicate, then record it — without locking.

// ❌ WRONG
isDuplicate, _ := p.IsDuplicate(ctx, txID) // Check: not duplicate
if isDuplicate {
return &PaymentResult{Success: true}, nil
}
// ... context switch; another goroutine also checks and finds not duplicate ...
err := p.recordPayment(ctx, req) // Act: record (but another thread also records!)
// → DOUBLE-CHARGE

Fix: Use distributed lock.

// ✓ CORRECT
lock, _ := locker.TryLock(ctx, lockKey)
defer lock.Release(ctx)
isDuplicate, _ := p.IsDuplicate(ctx, txID) // Check + Act both inside lock
if isDuplicate {
return &PaymentResult{Success: true}, nil
}
err := p.recordPayment(ctx, req) // Only one thread executes this

See: Distributed Locks For Payments


Concurrent Activation

Pitfall: Multiple payment confirmations (webhook + polling) both activate the same subscription.

// Time 1: Webhook arrives
payment1 := &PaymentRequest{InvoiceID: inv123, Source: PaymentSourcePrimerWebhook}
processor.RecordPayment(ctx, payment1) // Activates subscription
// → Chargebee: subscription.start_date = now, status = active
// Time 1.5: Polling worker also sees same invoice
payment2 := &PaymentRequest{InvoiceID: inv123, Source: PaymentSourcePrimerPolling}
processor.RecordPayment(ctx, payment2) // Activates again?
// → Without lock: double-activation (idempotency fails)
// → Chargebee: start_date updated twice (not terrible, but unsafe)

Fix: Distributed lock serializes all payment attempts.

// payments/processor.go - RecordPayment always acquires lock first
lock, _ := locker.TryLock(ctx, locks.InvoiceLock(req.InvoiceID))
defer lock.Release(ctx)
// Inside lock: only one thread proceeds
isDuplicate, _ := p.IsDuplicate(ctx, req.TransactionID)
if isDuplicate {
return &PaymentResult{Success: true, AlreadyProcessed: true}, nil
}
// Safe to record; no other thread can interfere

Concurrent Cleanup

Pitfall: Cleanup worker and user action both cancel the same subscription.

// Time 1: Cleanup detects expired checkout
expiredSub := findExpiredCheckout()
cancelSubscription(expiredSub.ID) // Sets status = cancelled
// Time 1.5: User tries to reactivate (didn't know it was expired)
reactivateSubscription(expiredSub.ID) // status = non_renewing → active
// → User gets access to expired subscription!

Fix: Validate subscription state before cancellation; use transactions if supported.

// ✓ Safe approach
lock, _ := locker.TryLock(ctx, locks.SubscriptionLock(subID))
defer lock.Release(ctx)
sub, _ := getSubscription(subID)
if sub.Status != "future" || sub.StartDate > now - 1*year {
return // Not a future subscription, or not expired; skip
}
cancelSubscription(subID) // Now safe

Timezone Gotchas

Chargebee Stores UTC; Comparisons Fail in Local Time

Pitfall: Query subscriptions created “today” without considering timezone.

// ❌ WRONG
today := time.Now() // Local time
startOfDay := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.Local)
endOfDay := startOfDay.AddDate(0, 0, 1)
// Query Chargebee (returns UTC times)
subs, _ := listSubscriptionsCreatedBetween(startOfDay, endOfDay)
// startOfDay is 2026-04-08 09:00:00 PDT = 2026-04-08 16:00:00 UTC
// endOfDay is 2026-04-09 09:00:00 PDT = 2026-04-09 16:00:00 UTC
// But subscriptions created at 2026-04-08 00:00:00 UTC don't match!

Fix: Always convert to UTC and be explicit.

// ✓ CORRECT
today := time.Now().UTC()
startOfDay := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, time.UTC)
endOfDay := startOfDay.AddDate(0, 0, 1)
subs, _ := listSubscriptionsCreatedBetween(startOfDay, endOfDay)
// Now timezone-aware

Renewal Times Drift Across Timezones

Pitfall: User in PST renews on the “same date” as user in EST, but they’re actually different dates in UTC.

User A (PST): 2026-04-08 23:59:59 PST = 2026-04-09 06:59:59 UTC
User B (EST): 2026-04-08 00:00:01 EST = 2026-04-08 04:00:01 UTC
Both think they renewed on 2026-04-08, but in Chargebee (UTC) they're different dates.
Dunning worker runs at 2026-04-08 12:00 UTC:
- User A's renewal is in the future (not yet due)
- User B's renewal is overdue

Mitigation: Chargebee stores everything in UTC; don’t try to be timezone-smart. Use renewal date from Chargebee, not local calculations.


Free Checkouts (100% Discount)

Pitfall: Granting Access Without Chargebee Confirmation

Pitfall: Coupon gives 100% discount → amount = 0 → skip Primer → immediately grant access → user cancels on the other side → refund issued → user still has access.

// ❌ WRONG
if discountedAmount == 0 {
grantAccess(userID) // Grant access immediately
// But what if Chargebee payment fails? User has access!
}

Fix: Record payment in Chargebee even for zero-amount invoices.

// ✓ CORRECT
// Create invoice in Chargebee with 0 amount
invoice, _ := createInvoiceForItems(customerID, items, coupon)
// Record payment (even though amount = 0)
processor.RecordPayment(ctx, &PaymentRequest{
InvoiceID: invoice.ID,
Amount: 0,
Source: domain.PaymentSourceFreeCheckout,
})
// EventPoller will process the invoice and apply credits
// Only then is access granted

Pitfall: Saved Payment Method Not Verified for Free Checkouts

Pitfall: 100% discount checkout succeeds without saved payment method → user later expects dunning retry on failed renewal.

User applies 100% coupon, no saved card:
✓ Checkout succeeds (free)
✓ Subscription created, activated
✓ Access granted
Subscription renewal comes due:
✗ No saved card for dunning retry
✗ User unaware renewal failed (expected renewal to work)

Mitigation: For free checkouts, require saved payment method (Primer vaulting) OR explicitly document the limitation.

// Optional: Validate that user has saved card
if discountedAmount == 0 && !userHasSavedPaymentMethod(userID) {
return errors.New("100% discount requires saved payment method for renewals")
}

Coupon Issues

Pitfall: Coupon Stacking

Pitfall: User applies multiple coupons; Chargebee allows it → discount is larger than expected.

// Coupon 1: 10% off
// Coupon 2: 10% off
// Both applied → 19% total discount (10% + 10% of remainder)
// Expected: 10% total

Mitigation:

  1. Chargebee’s standard behavior: “Last coupon wins” (only one active at a time)
  2. Our validation: validatecoupon.go checks if coupon is applicable
  3. Never add coupons together; let Chargebee handle the limit

See: validatecoupon.go for coupon validation logic


Pitfall: Expired Coupon Applied After Subscription Creation

Pitfall: Coupon valid when subscription is created, expires before payment is recorded.

Time 1: Validate couponCoupon expires 2026-04-10
Time 2: Create pending subscription with coupon
Time 3: User completes payment (2026-04-11)
Coupon is now expired!
But Chargebee invoice already has coupon applied
What discount does user get?

Chargebee behavior: Coupon is applied at invoice creation time (subscription creation). If it expires before payment, the discount is still honored on that invoice. This is correct (honors what was valid at checkout time).

Mitigation: Document that coupons are validated at subscription creation, not payment recording. If user delays payment and coupon expires, the original discount applies anyway.


Currency Issues

Pitfall: Currency Mismatches

Pitfall: User in EUR, selects USD pricing, currency gets confused.

// User is in EU (default EUR)
// User clicks "USD plan" button
// Frontend sends item_price_id = "plan-Starter-USD-Monthly"
// But user's billing address is in EUR country
// Chargebee creates subscription in USD
// But user's account says EUR
// Revenue report is confused (double-count?)

Mitigation:

  1. Frontend should respect user’s timezone/currency preference
  2. ListProducts() returns prices in all currencies; frontend selects
  3. Server should validate that selected currency matches user’s account

See: products/list.go for product listing with currency handling


Pitfall: Cross-Currency Coupons

Pitfall: Coupon is valid in USD only, user applies it to EUR subscription.

// Coupon: "10% off USD plans only"
// User: Tries to apply to EUR subscription
// Chargebee: Silently ignores coupon (no error)
// User: Unaware coupon didn't apply

Mitigation: Validate coupon against specific item_price_id (currency-specific).

// validatecoupon.go should check:
if !coupon.ApplicableTo(itemPriceID) {
return errors.New("coupon not applicable to this plan")
}

Renewal Credits & Proration

Pitfall: Renewal Credits Not Subtracted From Upgrade

Pitfall: User has 3 months left on annual plan, upgrades to premium. Should get prorated credit for unused time. If not subtracted, over-charges.

Annual Starter: $348/year, 3 months remaining = $87 credit
Premium upgrade: $1188/year, prorated to 3 months = $297
User should pay: $297 - $87 = $210
If credit not subtracted: User pays $297 + $87 = ❌ WRONG

Mitigation: Chargebee handles this automatically if invoice_immediately = true during subscription change. The created invoice includes both the credit and the prorated charge.

See: payments/processor.go:ActivateSubscription() - sets InvoiceImmediately: true


Idempotency Key Pitfalls

Pitfall: Transaction ID Not Globally Unique

Pitfall: Same transaction ID used for different payments.

// Primer transaction: "12345"
// User applies to invoice_A
// Later, user applies same "12345" to invoice_B (copy/paste error?)
// Both invoices marked as paid with same transaction ID
// Idempotency check: "Transaction 12345 already processed" → skips invoice_B
// → invoice_B not recorded; user gets no credits

Mitigation: Transaction ID must be globally unique across all payments. Use:

  • Primer transaction ID (already unique)
  • If manual: generate UUID and require
// PaymentRequest validation
func (r *PaymentRequest) Validate() error {
if r.TransactionID == "" {
return errors.New("transaction_id required")
}
// Assume it's already globally unique (from Primer)
return nil
}

Pitfall: Idempotency Key Lost on Retry

Pitfall: Webhook arrives, we record payment with transaction ID. Webhook retried by Primer, but our request handler doesn’t preserve the original transaction ID.

// Primer Webhook 1: transaction_id = "tx_abc123"
// We record payment, store transaction_id in DB
// Primer Webhook 1 Retry (Primer auto-retry): transaction_id = "tx_abc123"
// If we generate NEW transaction ID on each call: "tx_xyz789"
// → Duplicate payment! (Different transaction ID, same underlying Primer tx)

Mitigation: Always use Primer’s transaction ID, never generate a new one.

// submitpaidinvoice.go or primer webhook handler
func handlePrimerWebhook(w http.ResponseWriter, r *http.Request) {
var event PrimerEvent
json.Unmarshal(r.Body, &event)
// Use Primer's ID directly; don't generate new one
result, _ := processor.RecordPayment(ctx, &PaymentRequest{
TransactionID: event.TransactionID, // ← From Primer, globally unique
InvoiceID: event.OrderID,
Amount: event.Amount,
Source: domain.PaymentSourcePrimerWebhook,
})
}

Pitfall: SDK Global State

Pitfall: Chargebee SDK has global state (API key) that gets confused if multiple environments run in the same process.

// Process starts with API key for production
chargebee.SetAPIKey(prodKey)
// Test suite runs, temporarily sets test API key
chargebee.SetAPIKey(testKey)
// Main code runs again... which API key is active?
// → API calls may go to wrong environment

Mitigation:

  1. Don’t use SDK global state; wrap in a client struct (already done: chargebee/client.go)
  2. Each handler gets its own client instance
  3. Tests use mock clients, not real SDK

See: chargebee/client.go - wraps SDK in a Client struct


Database Transaction Pitfalls

Pitfall: Chargebee Change + DB Transaction Mismatch

Pitfall: Chargebee update succeeds, but DB transaction fails. Now they’re out of sync.

// Step 1: Update Chargebee
chargebee.UpdateSubscription(subID, params) // Status updated
// Step 2: Start DB transaction
tx := db.BeginTx(ctx)
tx.UpdateLocalSubscription(subID, ...)
tx.Commit() // ❌ FAILS (network error)
// Result: Chargebee has new state, DB has old state
// Next query: DB shows old state; user sees inconsistency

Mitigation: Use saga pattern. Update Chargebee first, then DB. If DB fails, saga is incomplete but safe to retry.

// ✓ Correct order
// Step 1: Update Chargebee (external)
chargebee.UpdateSubscription(subID, params)
// Step 2: Record in DB (internal)
// If this fails, Chargebee has committed but DB hasn't
// Next retry will see the Chargebee update and catch up
db.UpdateLocalSubscription(subID, ...)

See: Saga Pattern For Distributed Payments


New Type Boundaries

Pitfall: Money.Multiply() Isn’t Currency-Aware

Pitfall: Multiply USD amount by 12 (yearly multiplier), forget that different currencies have different precision.

// USD: $100.00 = 10000 cents
// JPY: ¥10000 = 10000 (no cents; 1 yen is smallest unit)
// Code assumes cents everywhere:
amount := 10000
amount = amount * 12 // = 120000
// For USD: Correct (= $1200.00)
// For JPY: ❌ WRONG ($1200 instead of ¥120000)

Mitigation: Chargebee handles this. All amounts are in the smallest currency unit. We just multiply; don’t overthink.

domain/entities.go
type Money struct {
Amount int64
Currency string
}
func (m *Money) Multiply(factor int) *Money {
// Chargebee handles precision; we just multiply
return &Money{
Amount: m.Amount * int64(factor),
Currency: m.Currency,
}
}

Pitfall: Credit Amount Overflow

Pitfall: Large credit amounts overflow int64.

// User's credit: 9223372036854775807 (max int64)
// Add 1 more credit → wraps to -9223372036854775808
// User suddenly has negative credits; exploit!

Mitigation: This is unlikely in practice (would require 9 exabytes of credits), but validate on large transfers.

func (c *CreditBundle) Add(other *CreditBundle) *CreditBundle {
images := c.Images + other.Images
if images > MaxCredits {
log.Warn().Msg("credit overflow prevented")
images = MaxCredits
}
// ...
}

API & Chargebee Quirks

Pitfall: Chargebee Returns nil for “Not Found”

Pitfall: Chargebee API returns nil object for 404 (not found), not an error.

result, err := chargebee.RetrieveSubscription(subID)
// If subscription doesn't exist:
// result.Subscription = nil
// err = nil
// ❌ Code assumes `err != nil` means not found
if err != nil {
return errors.New("subscription not found") // Never executed!
}
// Code continues with result.Subscription = nil
// → Panic on nil dereference

Fix: Always check for nil object.

result, err := chargebee.RetrieveSubscription(subID)
if err != nil {
return fmt.Errorf("failed to retrieve: %w", err)
}
if result.Subscription == nil { // ← Always check
return domain.ErrSubscriptionNotFound
}
// Safe to use result.Subscription

Pitfall: Chargebee Timestamps Are Seconds, Not Milliseconds

Pitfall: Chargebee returns Unix timestamps in seconds; code assumes milliseconds.

// Chargebee: 1712592000 (April 8, 2026 noon UTC)
// Code: time.Unix(0, 1712592000) // April 1, 1970 + 1.7s
// ❌ Off by 50+ years!
// Correct: time.Unix(1712592000, 0)

Mitigation: Always use time.Unix(secondsSinceEpoch, 0) when parsing Chargebee timestamps.


Pitfall: Chargebee Enum Values Are Strings, Not Constants

Pitfall: Comparing Chargebee status strings without using enums.

// ❌ WRONG (easy to typo)
if sub.Status == "actve" { // Typo: "actve" instead of "active"
// This block never executes
}
// ✓ CORRECT
if sub.Status == subscriptionEnum.StatusActive {
// Using SDK enum constant
}
// ✓ ALSO CORRECT (in domain layer)
if domain.SubscriptionStatus(sub.Status) == domain.SubscriptionStatusActive {
// Using domain enum
}

Testing & Monitoring Pitfalls

Pitfall: Tests Pass Locally, Fail in CI (Timezone)

Pitfall: Test assumes local timezone, but CI runs in UTC.

// ❌ WRONG
func TestRenewalDate(t *testing.T) {
today := time.Now()
expectedRenewal := today.AddDate(0, 1, 0) // 1 month from now
// Passes locally (local tz); fails in CI (UTC tz)
}
// ✓ CORRECT
func TestRenewalDate(t *testing.T) {
today := time.Now().UTC()
expectedRenewal := today.AddDate(0, 1, 0)
// Works everywhere
}

Pitfall: Mock Doesn’t Match Real API

Pitfall: Mock Chargebee client doesn’t enforce constraints that real API does.

// Mock allows:
mock.CreateSubscription(customerID="", itemPriceID="") // ✓ Mock doesn't care
// Real API rejects:
chargebee.CreateSubscription(customerID="", itemPriceID="") // ❌ Error: required field
// Test passes with mock, fails with real API

Mitigation: Test with real Chargebee (sandbox) for integration tests. Mocks only for unit tests.


Pitfall: Monitoring Doesn’t Catch Silent Failures

Pitfall: Event polling succeeds (returns 0 events) but Chargebee is actually down.

// Poller runs every 15 seconds
result, err := poller.Poll(ctx, opts)
if err != nil {
log.Error("poll failed") // Logged
// But if no error (e.g., Chargebee returns 500 silently as empty result set)
// Poller thinks: "No new events" (correct)
// But actually: "API down, all events missed" (not detected)
}

Mitigation: Add healthcheck endpoint; monitor API latency; alert if poll returns empty for >5 minutes.


Authorization Attempt Quota Pitfalls

CIT/MIT Buffer Asymmetry

Pitfall: Changing CIT limits without updating MIT limits (or vice versa) breaks the reservation invariant.

CIT enforces limit - reservePaymentAttemptMITBuffer (4/19/29). MIT enforces the full limit (5/20/30). If you change one without the other, either MIT gets starved (CIT consumes all slots) or CIT reserves a slot nobody uses.

Constants to keep in sync: successfulPaymentDailyCountLimit (and weekly/monthly) in successful_payment_velocity.go with reservePaymentAttemptMITBuffer in reserve_payment_attempt.go.

VelocityGuard vs QuotaGuard Scope

Pitfall: Confusing which guard runs where.

GuardWhat it checksWhere it runs
VelocityGuard (EnforceHighRisk...)Count windows on authorization_attempt, amount windows on payment_successfulCIT checkout + manual MIT retry only
QuotaGuard (inline MIT gate)Count windows on authorization_attempt onlyAll MIT retries (manual + scheduled)
ReservePaymentAttemptAtomic count + insert authorization_attemptCIT checkout only (pre-checkout)

Removing or reordering these changes enforcement behavior. VelocityGuard runs only for manual retries. QuotaGuard runs for all MIT retries (manual and scheduled).

PSP Routing Fails Closed

Pitfall: resolveRenewalPSPRouting returns an error (not a default) when Chargebee is unreachable or subscription data is missing. This aborts the dunning retry. It looks like a bug but is intentional — routing to the wrong PSP (e.g., SFW Stripe for an NSFW subscription) is worse than a delayed retry.

Production-Only Enforcement

Pitfall: IsProd() gates all quota enforcement. Staging and development skip it entirely. If you need to test quota behavior locally, you must temporarily remove the IsProd() guard.

Quota-Blocked Dunning Attempts

Pitfall: If a quota-blocked MIT retry does NOT record a dunning_attempt, the DunningAttemptExists check returns false on the next poll cycle, causing the same blocked retry to fire every 2 min (dev) or 15 min (prod) until the dunning window expires. Blocked attempts must set attemptResult so the defer records the row.

Polling Cursor Must Not Advance Past Unresolved Payments

Pitfall: In TaskPollPrimerPayments, returning nil from processSettledPayment counts as success and lets the cursor advance past the payment. Outside the 5-minute overlap window the payment is never re-listed — “retry next tick” silently never comes. This is how orphan-recovery backoff waits used to truncate the 10-attempt budget to 2-3 on busy traffic (waits returned nil), and how a transient detail-fetch failure could permanently disable recovery for a payment.

Mitigation: Skip-but-retry-later states must surface the errOrphanRecoveryBackoff sentinel (or any error), and persistPollingCursor/clampCursorToUnresolved pins the cursor behind the oldest unresolved payment. Return nil only for genuinely terminal outcomes (processed, exhausted-and-alerted).

Logging: Backoff holds log at Debug (orphan recovery in backoff - holding cursor); real failures log at Error per tick.


NSFW Payment Method Pitfalls

Pitfall: Wallet Token Sent to Card-Only PSP

Pitfall: User’s primary payment method is Apple Pay or Google Pay. Scheduled NSFW renewal sends the wallet token to EMP (card-only). EMP hard-declines. Subscription enters dunning unnecessarily.

Mitigation: resolveNSFWRenewalPaymentToken in dunningretry.go detects this triple condition (scheduled + NSFW + wallet primary) and substitutes a saved card if one exists. If no card is saved, the wallet token is sent anyway (EMP rejects, dunning proceeds normally).

Logging: Look for nsfw_renewal_card_substitution (card found and swapped) or nsfw_renewal_no_card_found (no card available, proceeding with wallet).


Pitfall: NSFW Checkout Doesn’t Set Primary

Pitfall: User completes NSFW checkout with a new card. Primer vaults it but doesn’t set it as default. Next renewal uses the old primary (possibly a wallet), which EMP rejects.

Mitigation: maybeSetNSFWCheckoutPrimary in submitpaidinvoice.go auto-sets the checkout card as default after successful NSFW payment. Non-fatal — if Primer’s SetDefault API fails, the old primary remains.

Logging: Look for nsfw_checkout_set_primary (success) or nsfw_checkout_set_primary_failed (failure).


Pitfall: NMI Rejects Cross-PSP Vaulted Cards

Pitfall: Card was vaulted through Stripe (SFW checkout). User switches to NSFW tier. NMI cannot use a Stripe-vaulted token.

Mitigation: Frontend hides vaulted methods on SFW→NSFW tier switch (showVaultedMethods={false}), forcing fresh card entry through the NSFW checkout flow which vaults via the correct PSP.


Processor Assignment Pitfalls

Pitfall: Backfill Before Flipping decideProcessorForNewUser

Pitfall: Switching decideProcessorForNewUser to return NMI before backfilling existing users. Existing users with empty nsfw_processor would get NMI on next checkout, splitting them across two PSPs mid-subscription.

Mitigation: Two-release strategy. Release 1 defaults to EMP. Backfill all rows. Release 2 flips to NMI. Existing users keep EMP forever (write-once guard).

Pitfall: Renewal Creates Processor Assignment

Pitfall: If overrideProcessorFromCredits were to write (not just read), a renewal could assign a processor before the user goes through checkout, locking them to a PSP they never explicitly vaulted with.

Mitigation: Renewal path is read-only by design. Only ResolveProcessor (called during checkout/vaulting) writes assignments. overrideProcessorFromCredits only reads and falls back.

Pitfall: Write-Once Guard Depends on NULL/Empty Check

Pitfall: AssignSFWProcessor and AssignNSFWProcessor use WHERE sfw_processor IS NULL OR sfw_processor = ''. If a processor is set to a non-empty value by any path, it can never be overwritten through normal code.

Mitigation: This is intentional — processor lock is permanent. Manual SQL is the only override path, and should only be used for incident remediation.

Pitfall: NMI Per-Transaction Limit ($1499)

Pitfall: NMI payment processor has a hard per-transaction limit of $1499. Plans priced above this (e.g., NSFW Ultimate yearly at $1799) will fail at the processor level if not caught earlier.

Mitigation: CreatePrimerCheckout checks exceedsNMITransactionLimit() after processor resolution. If the user is routed to NMI and finalAmount > 149900 cents, checkout is rejected with codes.InvalidArgument / "plan price exceeds processor limit". The pending Chargebee subscription is rolled back. Frontend shows a user-friendly toast. No fraud events are recorded for blocked attempts.

If adding new high-priced plans: Verify the price stays under NMIMaxTransactionAmountCents (defined in domain/constants.go) or the plan will be unpurchasable for NMI-routed users.


Summary: Testing Checklist

When modifying billing code, verify:

  • No race conditions: Concurrent calls are serialized by locks
  • Idempotency: Same request processed twice → same result
  • Timezone-aware: All time comparisons in UTC
  • Chargebee nil-safe: Check for nil objects, not just errors
  • Currency-safe: Currency precision handled correctly
  • Coupon-safe: Coupons don’t stack unexpectedly
  • Free checkout tested: 100% discount works end-to-end
  • Renewal tested: Dunning retry succeeds/fails as expected
  • Quota guards: CIT reserves MIT buffer, MIT uses full limit
  • Monitoring: Metrics and logs sufficient to diagnose
  • Backward compatible: Old subscriptions still work
  • Processor assignment: Write-once guard holds, renewals don’t write, backfill before NMI flip