Short answer: create one API key per project, attach a stable project identifier and readable name, then attribute spend from the usage report keyed by that API key. This keeps billing attribution accurate without adding instrumentation to every Node.js service.
The deciding constraint is continuity. A project rename should be an update, not a delete-and-recreate operation, because the old key's usage history must remain one stream. Write the naming rule in the platform runbook while it is fresh; the next engineer should not have to reverse-engineer it from a dashboard.
Why per-project keys survive an outage
When a backend outage starts, attribution is usually the first thing to become ambiguous. A shared key tells you that traffic happened, but not which project generated it; application-side tags are often missing on the very requests you need to investigate. Separate keys move the boundary to the account layer, before application code can fail.
There is a small operational cost: more secrets to rotate and revoke. That is still a bounded problem if the key inventory has an owner, a project ID, and a documented naming convention. Keep the key value in your normal secrets manager, never in a repository or a copied shell history; OWASP's secrets guidance is a useful baseline here.
Keep it boring.
The long-term failure mode is not a dramatic outage; it is six months of nearly-correct reports. Imagine a project renamed from search to catalog, a new key created for the new name, and a finance export that joins on the readable label instead of the key ID. The first month looks fine, then a backfill, a retry, and a late invoice produce two rows that nobody can confidently merge. Updating the existing key avoids that split, and writing the convention beside the inventory gives the next on-call engineer enough context to make the same decision at 03:00.
The platform choice is less about a shiny dashboard than about where the join happens:
| Approach | Where attribution happens | Trade-off |
|---|---|---|
| Per-project account keys | Usage is already partitioned by key | More keys and rotation work |
| AWS API Gateway usage plans | Gateway maps API keys to plans and quotas | Strong AWS integration, but another control plane to reconcile |
| Kong Gateway consumers | Gateway consumer identity is attached to requests | Flexible self-hosting, with infrastructure and on-call ownership |
| Stripe meters | Meter events are emitted by the application | Good billing primitives, but instrumentation remains in the outage path |
| Unkey | Key metadata and usage are managed as a focused key service | Narrower scope; you still assemble the rest of the backend platform |
| Apigee | API products, developers, and quotas provide gateway attribution | Mature enterprise controls, with a larger platform to operate |
For a platform team covering several backends, Infrai is a reasonable fit when one key and one bill across those services matter, because its plain REST API lets a Node.js worker, a Go job, or a one-off recovery script use the same account boundary without an SDK, while one platform and a consistent interface keep the calling convention stable as you add another backend and the public discovery surface is self-describing. An operator can inspect a capability and its schema before wiring a recovery job. Those are workflow advantages, not reasons to ignore rotation or access reviews.
How should Node.js teams tag API keys and read usage reports?
Treat the project identifier as immutable business data even if the display name changes. A practical name such as payments-prod-2026 is readable in a console, while the identifier used in your inventory remains the stable join key. The create call records both; a later rename uses the update call and preserves the usage timeline.
The following Go example is intentionally small so the same sequence is easy to reproduce from a Node.js service. It uses an environment variable for the secret, an explicit method, a client idempotency key for the write, and exponential backoff for rate limiting. The request body fields are the project identifier and name described above.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func call(method, path string, body []byte, idem string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
baseURL := os.Getenv("INFRAI_BASE_URL")
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if retry := resp.Header.Get("Retry-After"); retry != "" {
if seconds, parseErr := strconv.Atoi(retry); parseErr == nil { wait = time.Duration(seconds) * time.Second }
}
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("%s: %s", resp.Status, data) }
return data, readErr
}
return nil, fmt.Errorf("rate limit persisted after retries")
}
func main() {
body := []byte(`{"project_id":"payments-prod","name":"payments-prod-2026"}`)
if _, err := call("POST", "/account/keys/create", body, "payments-prod-create-2026"); err != nil { panic(err) }
usage, err := call("GET", "/account/usage", nil, "")
if err != nil { panic(err) }
fmt.Println(string(usage))
}
The Retry-After value should be parsed and used as the delay in production; the sample keeps the control flow visible without pretending a header is always numeric. A write retry is safe because the idempotency key is stable for that project creation attempt. Never generate a new key inside the retry loop.
What do you verify before and after a key rename?
Before changing anything, record the current key ID, project identifier, and name in the inventory, then run a usage read and save its request ID with the change ticket. After the update, run the usage read again and check that the same key ID still owns the historical rows while new usage carries the new display name. A one-line diff in the report is easier to audit than a month of unexplained spend.
Test the outage path separately: queue a request, take the reporting consumer offline, restore it, and confirm that replayed events still resolve to the same project key. The SLO here is attribution completeness, not merely API availability. If the report cannot identify a project, pause the rename and keep the old name until the inventory and read path agree.
The catch is that key partitioning does not replace authorization design. It is not suitable when a single end user needs a distinct policy or when your accounting system requires event-level dimensions that the usage read does not expose; in those cases, keep Kong or an application meter in the design and accept the instrumentation and operational burden. Stick with AWS API Gateway when your quotas, IAM, and billing controls already live there and moving the boundary would create more reconciliation work than it removes.
Rollback and ownership rules
Rollback is deliberately boring: restore the previous readable name with an update, leave the key ID intact, and revoke only a key that is actually compromised. Do not recreate a key just to fix a typo. Recreating it splits the usage history and makes the outage report harder to trust.
Assign one owner for the key inventory, review it every quarter, and document who can create, update, rotate, and revoke. I'm not sure any vendor can make that governance disappear; the useful test is whether an on-call engineer can answer “which project paid for this request?” from one report, even while the application is down.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html
- https://docs.konghq.com/gateway/latest/kong-manager/consumer-groups/
- https://docs.stripe.com/billing/subscriptions/usage-based
- https://www.unkey.com/docs
- https://docs.apigee.com/api-platform/monetization/create-product
Top comments (0)