DEV Community

UlricDonovan1564
UlricDonovan1564

Posted on

Property Billing: How to Temporarily Raise Node.js API Spend at Launch

TL;DR: Treat a temporary API spend-cap increase as a lease, not as two unrelated configuration changes. Store the old cap, new cap, tenant ID, scoped key ID, start time, expiry, and restore status in one durable record; apply both transitions with idempotency keys; and let a reconciler restore every expired lease. A timer may reduce latency, but the database is the schedule. This preserves billing attribution during a growth spike even if the Node.js service or a queue worker restarts.

The operational trade-off is plain: a larger cap protects launch traffic, while a forgotten cap turns a 30-minute exception into an unbounded policy change. I have been paged for missed jobs and duplicate deliveries. Those two failure classes point to the same invariant here: expiry must survive process loss, and repeating a transition must be harmless.

Consider a property-management account platform issuing one scoped key per tenant. Tenant pm_4821 is launching online rent collection at 18:00 UTC. Its normal API budget is 120,000 units per hour; the approved launch budget is 300,000, expiring at 18:30 UTC. Usage must remain attached to key key_pm_4821_rent_v3, because a shared launch key would blur tenant-level billing attribution. The numbers are example policy data, not measured capacity guidance.

How should a launch temporarily raise and restore an API spend cap?

A setTimeout in the Node.js API process knows when to fire only while that process remains alive. Deployments, crashes, autoscaling, and a machine clock adjustment can separate the raise from the restore. A cron job that scans durable state is better, but cron alone can overlap: one run may still be working when the next begins. Queue delivery should also be treated as repeatable during failure recovery, so duplicate restore attempts are ordinary events, not edge cases.

Process memory is not approval state.

Use an absolute UTC expiry encoded as RFC 3339, persist it before changing the cap, and compare it against the database clock. The record is a lease with a small state machine: pending, active, restoring, restored, or failed. Do not infer success merely because a job was dequeued.

The tenant and key belong in the same record as the budget fields. This is the attribution boundary. If a tenant rotates its key during the launch, the lease still names the key whose usage was admitted under the elevated budget; the restore operation targets the tenant policy, while the audit event retains both old and new key IDs. Never place the key secret in this table. OWASP recommends centralizing secret management, applying least privilege, and automating rotation; store only a non-secret identifier here.

A minimal domain model keeps the contract visible:

package budgetlease

import "time"

type Lease struct {
    ID, TenantID, ScopedKeyID string
    NormalCap, TemporaryCap   int64
    StartsAt, ExpiresAt       time.Time
    Status                    string
    Version                   int64
}

func (l Lease) Valid() bool {
    return l.ID != "" && l.TenantID != "" && l.ScopedKeyID != "" &&
        l.NormalCap >= 0 && l.TemporaryCap > l.NormalCap &&
        l.ExpiresAt.After(l.StartsAt)
}
Enter fullscreen mode Exit fullscreen mode

Keep the duration bounded by policy outside this type. A 30-minute request should not silently become 30 days because a caller supplied the wrong unit. Reject an expiry in the past, require an approval reference, and record the authenticated actor before activating anything.

Implement the change as a durable lease

The write path has an awkward failure window: the policy service can accept the higher cap just before the local database transaction fails. A distributed transaction is rarely available across those boundaries. The practical answer is a recoverable workflow with stable operation IDs. Persist intent first, call the cap adapter, then mark the lease active. A reconciler can safely retry any incomplete step.

The adapter below is deliberately generic. Its implementation might call an internal policy service or update a local control plane. The important contract is conditional, idempotent application: the same operation ID produces the same outcome, and expectedCap prevents this lease from overwriting a newer human or automated decision.

package budgetlease

import (
    "context"
    "fmt"
)

type CapStore interface {
    Apply(context.Context, string, int64, int64, string) error
}

type LeaseStore interface {
    InsertPending(context.Context, Lease, string, string) error
    MarkActive(context.Context, string, int64) error
}

func Activate(ctx context.Context, leases LeaseStore, caps CapStore, l Lease, approval, actor string) error {
    if !l.Valid() {
        return fmt.Errorf("invalid budget lease")
    }
    if err := leases.InsertPending(ctx, l, approval, actor); err != nil {
        return fmt.Errorf("persist intent: %w", err)
    }
    if err := caps.Apply(ctx, l.TenantID, l.NormalCap, l.TemporaryCap, l.ID+":activate"); err != nil {
        return fmt.Errorf("apply temporary cap: %w", err)
    }
    return leases.MarkActive(ctx, l.ID, l.Version)
}
Enter fullscreen mode Exit fullscreen mode

There is a deliberate consequence: an ambiguous error from Apply cannot be treated as proof that nothing happened. Retry with the same operation ID, then read the authoritative cap. If it equals the temporary value, continue the workflow; if it equals the normal value, retry activation; if it equals neither, stop and alert because another policy change has intervened.

Unknown is a state.

I first reach for a scheduled queue message because it maps neatly to the expiry. The trap is letting that message become the only copy of the schedule. Retention changes, a poisoned message, or an operator purge can erase it. The lease row remains the source of truth; delayed delivery is only a wake-up hint.

Reconcile expiry without clobbering a newer cap

Run a small reconciler continuously or on a frequent schedule. Each pass claims a limited batch of expired active leases using a database transaction and row locking. Multiple workers may run, but only one claims a given version. After the external restore succeeds, the worker marks the lease restored.

Do not write NormalCap unconditionally. Imagine that the launch team receives a separately approved permanent increase to 180,000 at 18:20 UTC. A blind restore at 18:30 would erase that change. Compare-and-set makes the conflict explicit: restore 120,000 only if the current value is still the lease's temporary 300,000. Otherwise, preserve the current value, mark the lease as requiring review, and emit an alert with no secret material.

package budgetlease

import (
    "context"
    "fmt"
    "time"
)

type ExpiryStore interface {
    ClaimExpired(context.Context, time.Time, int) ([]Lease, error)
    MarkRestored(context.Context, string, int64) error
    MarkFailed(context.Context, string, int64, string) error
}

func Reconcile(ctx context.Context, store ExpiryStore, caps CapStore, now time.Time) error {
    batch, err := store.ClaimExpired(ctx, now.UTC(), 100)
    if err != nil {
        return fmt.Errorf("claim expired leases: %w", err)
    }
    for _, l := range batch {
        err := caps.Apply(ctx, l.TenantID, l.TemporaryCap, l.NormalCap, l.ID+":restore")
        if err != nil {
            _ = store.MarkFailed(ctx, l.ID, l.Version, "restore did not complete")
            continue
        }
        if err := store.MarkRestored(ctx, l.ID, l.Version); err != nil {
            return fmt.Errorf("record restore for %s: %w", l.ID, err)
        }
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The fixed batch size bounds each pass. Tune cadence and batch size from the restore-lag objective and observed work rate, not from the launch forecast alone. Backoff should be capped so retries still fit inside that objective.

Short version: restore is a conditional state transition. Not a cleanup callback.

Make billing attribution testable

The system needs evidence beyond a green job status. For every transition, emit an audit event containing the lease ID, tenant ID, scoped key ID, actor or worker identity, approval reference, requested cap, prior cap, effective cap, event time, operation ID, and outcome. Exclude credentials and raw authorization headers. The scoped key ID should also appear on metering records, allowing finance to join usage to the exact tenant and lease window.

Three signals catch different failures:

  • budget_lease_restore_lag_seconds measures the distance between expiry and confirmed restoration. Alert on sustained lag, not a single slow call.
  • Counts of active leases past expiry detect missing work even when the worker emits no errors. This is the dead-man check.
  • Conditional-update conflicts reveal legitimate concurrent policy changes and possible control-plane races. Route them for review rather than retrying forever.

Test the state machine with a fake clock and fault injection. Kill the worker after the cap adapter succeeds but before MarkRestored; the next pass must repeat the same operation ID and finish. Deliver the same expiry hint twice. Rotate the scoped key during the lease. Apply a permanent cap change before expiry and verify that restore reports a conflict instead of overwriting it. Finally, advance the clock across expiry without running any timer and confirm that reconciliation finds the row.

A staging test should use isolated tenant and key identifiers so its usage cannot contaminate production attribution. For deployment, start the reconciler in observe-only mode, compare the leases it would claim against approved records, then enable writes for a small tenant cohort. Keep the old-cap value and audit trail for the retention period required by your billing and security policies.

When should you use a simpler mechanism?

If the upstream API already provides an atomic, durable temporary-limit primitive with an expiry and an audit log, use that primitive and reconcile its reported state. Reimplementing the timer adds failure modes. Verify what happens during control-plane downtime and whether a later policy edit supersedes the scheduled restore.

A manual restore can be reasonable for a low-risk sandbox with no billable traffic, provided the exception cannot affect production credentials. It is a poor fit for tenant billing. Likewise, a single-process timer is acceptable for a local development tool whose state may disappear with the process.

For production property-management traffic, the decision rule is stricter: use a durable lease when the elevated cap changes financial exposure, when attribution depends on a tenant-scoped key, or when a missed restore needs to be detectable without relying on the worker that missed it. The lease record, conditional adapter, and reconciler form one control. Remove any one of them and a familiar incident class returns.

Sources

Top comments (0)