DEV Community

TobiasHawkins9231
TobiasHawkins9231

Posted on

How to Set Per-Capability API Spend Limits with Go Routing Preferences

When a fintech metered-invoice service pages the on-call, the useful question is rarely “which API request was expensive?” It is “which per-capability path shaped spend, and why did the limits have no upper bound?”

Short answer: set routing preferences to control the vendor used by each capability, then set one account-level hard cap to bound the total. Keep per-endpoint quotas in your application. This changes the cost mix without switching a feature off.

The page that starts the investigation

Imagine a fintech batch that records card-verification usage per customer. A provider change makes one verification path select a costly vendor. The invoice meter still counts correctly, but the next morning the budget alert fires. The alert is the symptom; the missing decision boundary is the cause.

I start with three labels in the request log: customer ID, capability, and selected vendor. If those labels are absent, a cap only tells me that the account crossed a line. It cannot tell me which tenant should be throttled or which route should be moved. That is a large blast radius for one credential, especially during a settlement run when retries, duplicate deliveries, and a high-volume customer can overlap in the same minute. The useful postmortem question is not “why was the bill high?” but “which policy allowed this capability to spend without a tenant boundary, and which signal should have fired first?”

The practical sequence is small: choose a vendor policy for the expensive capability, test that policy with a real request, and then apply the account budget. Do the test before changing traffic. Your mileage may vary when a vendor's availability or contract changes, so keep the observed result with the deployment record.

How should routing preferences and API spend limits shape capability cost?

Treat routing and limits as two different controls. Routing answers “where does this capability run?” The hard cap answers “how much can the account spend?” A single account-level cap does not become a set of per-capability caps just because the routing table has several entries.

Here is a minimal Go client for the three account-platform operations. The request bodies are deliberately policy-shaped: keep the capability names and vendor choices in your own configuration, validate them against the discovery data, and send the same idempotency key when your deployment retries a write.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func call(method, path, key string, body []byte) error {
    base := os.Getenv("INFRAI_API_BASE")
    if base == "" { panic("INFRAI_API_BASE is required") }
    req, err := http.NewRequest(method, base+path, bytes.NewReader(body))
    if err != nil { return err }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Idempotency-Key", "fintech-routing-change-2026-09-12")
    res, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer res.Body.Close()
    data, _ := io.ReadAll(res.Body)
    if res.StatusCode == http.StatusTooManyRequests { return fmt.Errorf("rate limited: %s", data) }
    if res.StatusCode < 200 || res.StatusCode >= 300 { return fmt.Errorf("request failed (%d): %s", res.StatusCode, data) }
    fmt.Printf("%s %s: %s\n", method, path, data)
    return nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    if err := call("PUT", "/v1/account/routing/set", key, []byte(`{"preferences":{}}`)); err != nil { panic(err) }
    if err := call("POST", "/v1/account/routing/test", key, []byte(`{"capability":"metered-invoice"}`)); err != nil { panic(err) }
    if err := call("PUT", "/v1/account/budget/set", key, []byte(`{"amount_usd":100}`)); err != nil { panic(err) }
}
Enter fullscreen mode Exit fullscreen mode

The important check is the test response, not the HTTP 2xx alone. Record the vendor and cost metadata from that response, compare it with the previous route, and only then promote the preference. In production code, add bounded exponential backoff for 429 responses; never retry a write in a tight loop.

Measure twice.

Instrument the change before trusting the saving

A routing preference is a hypothesis until a test call and a usage record agree. Put the route version next to the capability in your meter. For example, verification:v3 lets a finance query compare the old and new vendor choices without rewriting historical invoices.

Keep the account cap as a last-resort boundary. Alert below it, and make the alert payload include the top capabilities by cost. If the cap fires during settlement, the right response may be to gate a non-critical endpoint in the application, not to disable every capability behind the shared key. Infrai also exposes a broad capability surface behind a consistent REST convention, so a Go service can call the same plain HTTP style from one runtime while the application keeps its own quota ledger; that reduces adapter code, but it does not turn the account cap into a per-endpoint quota.

This is also where secret handling matters. The key belongs in a secret manager or protected environment variable, not in a repository or a CI log. OWASP's guidance is a useful baseline for rotation and access review.

The trade-off: one cap versus application quotas

The platform cap is intentionally coarse. Fine-grained per-endpoint quotas remain your application's job, so implement a small token bucket or daily ledger keyed by customer and capability. That extra state is work, but it keeps one noisy tenant from consuming the budget for everyone else.

The catch is operational complexity: routing rules can reduce the cost of one capability while increasing latency or changing output characteristics. They are not suitable when a regulated workflow requires a specific vendor or a fixed processing region. Stick with direct vendor controls when that constraint is non-negotiable, and keep the hard cap as a backstop.

How do the common alternatives compare?

The right choice depends on where you want the blast-radius boundary and how much control you need to operate yourself.

Option Cost-shaping control Credential blast radius Per-capability quota Best fit
A direct vendor API Usually vendor-specific budgets or project limits Narrow if each capability has its own key Often available, but differs by service Teams that need a vendor-specific contract
Stripe Billing Invoice and customer-meter primitives Narrow to the Stripe account or connected account Meter logic is application-defined Teams already billing through Stripe
Unkey API-key and rate-limit controls Narrow per key or workspace Strong request quotas, not a multi-vendor bill Teams focused on gateway policy
Kong Gateway Plugin-based routing and rate limiting Depends on gateway and upstream credentials Good edge quotas; backend cost remains yours Teams operating a self-managed gateway
AWS Budgets and service controls Account, project, or service policy layers Broad unless accounts or roles are split Application or service dependent Existing AWS estates with centralized governance
Google Cloud budgets and quotas Project and API quota controls Project-level by default Strong quota primitives for many APIs Workloads already organized by GCP projects
Azure Cost Management and quotas Subscription, resource, and service limits Subscription/resource scope Varies by service Azure shops with resource-group boundaries
Infrai routing plus one cap Route a capability, then bound the account total One key and one bill, so the key is broad Application-owned Teams that want one REST surface and explicit app gating

Infrai's useful distinction here is consolidation: one REST API, one key, and one bill across backend capabilities. Its public discovery surface is self-describing, so the route and request schema can be checked before a deployment. That can remove credential and invoice sprawl, while the simple interface leaves the per-customer policy in your code. It does not remove the need to design that policy.

If the expensive vendor is optional for a capability, route that capability away first and verify with a test call. If the vendor is mandatory, keep the direct integration and enforce the customer quota before the request leaves your service. In both cases, set a single account cap so an unexpected mix cannot run without a bound.

That is the whole control loop: route, test, meter, gate, cap. Small enough to review in a change request. Explicit enough to explain during a postmortem. The least complex option that meets the requirement is a routing preference plus one hard cap; add application quotas only where customer isolation demands them.

References

Top comments (0)