Gateway Operations Runbook
Diagnose dependency incidents, capacity protection, routing loops, ambiguous mutations, and Gateway alerts without replaying unsafe work.
This runbook is for Duplo engineers and integration operators supporting the NRS Invoice Gateway. It explains which component owns a failure, which evidence to collect, and when a retry would be unsafe. Customer applications should still make decisions from the public GW_* Problem Details contract.
Do not troubleshoot by replaying a mutation
A timeout or broken connection does not prove that NRS, Spend, or the
administrator service did nothing. Freeze a sign or transmission that
returns an unconfirmed-outcome code. Search by requestId and operationId
before considering another call.
Failure ownership at a glance
| Symptom | Gateway behavior | NRS called? | First owner action |
|---|---|---|---|
| Spend domain registry is unavailable and the domain is not cached | 503 GW_DOMAIN_REGISTRY_UNAVAILABLE | No | Restore Spend registry access; caller may back off with the same mutation identity. |
| Spend domain registry is unavailable but a domain snapshot is cached | Gateway still requires fresh tenant authorization; an authorization outage returns 503 GW_TENANT_AUTHORIZATION_UNAVAILABLE | No | Restore Spend authorization. Do not enable stale authorization as a workaround. |
| Customer API key is invalid | 401 GW_API_KEY_INVALID | No | Correct or rotate the customer's key. This is not a dependency incident. |
| Gateway-to-Spend credential is rejected | Dependency failure, not a customer 401 | No | Correct the deployed service credential and inspect the immediate alert. |
| Administrator backend is unavailable before a debit is confirmed | 503 GW_BILLING_SERVICE_UNAVAILABLE | No | Restore the administrator backend; retry only as the response directs, retaining the same key and content. |
| Administrator or recovery work fails after a debit | A sign recovery or reversal code is returned; the original attempt remains under reconciliation | Maybe | Do not resubmit. Work the billing recovery queue and correlate the operation. |
| NRS times out on an ordinary safe read | 504 GW_UPSTREAM_TIMEOUT | Maybe | Retry only a documented read with bounded backoff. |
| NRS has a transport or 5xx failure on an ordinary route | 502 GW_UPSTREAM_UNAVAILABLE | Maybe | Follow the route-specific retry rule; do not apply a generic retry to a mutation. |
| NRS throttles a child-bound call | 429 GW_RATE_LIMIT_EXCEEDED with Retry-After | Yes | Wait for the supplied interval and reduce concurrency. |
| NRS rejects Gateway-owned provider credentials with 401/403 | Operator-facing regulator/dependency failure | Yes | Rotate or correct the deployed NRS credential. Do not tell the customer to change their invoice or API key. |
| NRS sign result cannot be proved | 502 GW_SIGN_OUTCOME_UNCONFIRMED | Maybe | Freeze the IRN and reconcile NRS plus billing state. |
| NRS transmission result cannot be proved | 502 GW_TRANSMIT_OUTCOME_UNCONFIRMED with operationId | Maybe | Do not transmit again; reconcile the durable transmission claim. |
| Spend processor is down after Stored intake | The original intake can remain 202 Accepted; the invoice stays pending | Not synchronously | Spend owns processor-heartbeat, queue-age, and asynchronous administrator/NRS alerts. Do not create a replacement invoice. |
| Administrator UI alone is unavailable | No execution impact | Unchanged | Restore the UI separately; the backend dependency determines Gateway behavior. |
Fresh Spend authorization is a safety requirement. Gateway must not serve an authenticated tenant from stale authorization data during a Spend outage.
Trace one request end to end
- Obtain the Gateway-generated
X-Request-IDor Problem DetailsrequestId. A caller-suppliedX-Request-IDis optional metadata and is not the authoritative identifier. - For an ambiguous or recoverable mutation, also retain
operationId, the stable idempotency key, IRN, represented-business ID, target state, and UTC time. - Find the
gateway_request_completedJSON event byrequestId. Confirm route, state, mode, status, public error code, duration, and response size. - Follow
gateway_dependency_callevents with the same ID. Inspect dependency, operation, latency, upstream status, normalized failure class, contract version, provider correlation ID, and circuit state where present. - For a sign, inspect debit, reversal, recovery-marker, and reconciliation state. For a transmission, inspect the durable claim by
operationId. Never infer “not committed” from a missing response. - Use the code-specific action in Gateway error responses. Escalate with the safe support packet, not raw headers or invoice data.
Gateway logs and alert signals must never contain API keys, authorization headers, NRS passwords, private keys, raw idempotency keys, invoice bodies, unredacted query strings, taxpayer data, or upstream response bodies. Hash an idempotency key when correlation is necessary.
Health endpoints
| Endpoint | Access | Meaning | Must not be used as proof of |
|---|---|---|---|
GET /health | Public | Compatibility liveness; returns { "status": "ok" } while the process can answer. | Spend, NRS, billing, wallet, or represented-business readiness. |
GET /health/live | Public | Process liveness; returns { "status": "ok" }. | Dependency health. |
GET /health/ready | Public/probe | Local readiness and the Redis safety check. Returns 200 ready or 503 not_ready. | A globally healthy Spend or NRS. Shared dependency incidents intentionally do not remove every Gateway replica. |
GET /health/dependencies | Private operator route through the verified edge with x-admin-api-key | Cached dependency, circuit, capacity, and recovery-backlog view. It never fans out to every dependency per request. | A customer-facing service-level commitment or a live NRS probe. |
The protected dependency snapshot can include Spend registry, Spend authorization, Spend management, administrator, NRS test/live, Redis, Slack, processor-heartbeat, circuit state, contract version, in-flight capacity, and recovery counts. unknown means no trustworthy observation has been reported; it is not equivalent to healthy. The Spend processor may remain unknown until Spend publishes its heartbeat to Gateway.
Capacity, rate, and circuit protection
Gateway applies controls in layers so one domain cannot occupy every worker or outbound socket:
- Canonical host, route, content type, declared body size, and routing-loop checks run before dependency work.
- Distributed early admission control keys requests by canonical domain and a client identity supplied by the trusted edge. The edge must overwrite that header; caller
X-Forwarded-Forvalues are not trusted. - A process-local, no-wait capacity guard caps total and per-domain in-flight work. A full guard returns
503 GW_GATEWAY_CAPACITY_EXCEEDEDwithRetry-Afterinstead of building an unbounded queue. - After authentication, route-specific quotas apply. Sign remains 120 requests per 60 seconds and management 30 per 60 seconds by default. Other route budgets are enabled only after observation and load testing.
- Dependency bulkheads cap concurrent calls separately from Gateway admission. Current safe defaults are configurable and must be validated against the deployed 1 CPU/1 GiB pod profile, outbound socket pool, and downstream capacity.
Redis backs fleet-wide admission and mutation coordination. Protected work fails closed with structured 503 when Redis cannot enforce a safety decision; local hard caps remain in place to protect the process.
The same edge-owned client address is carried over Gateway's authenticated internal authorization hop for credential IP allowlisting. An empty ipWhitelisted list is unrestricted. With a non-empty list, a missing, malformed, or non-matching trusted address fails authorization with 403; never bypass that failure by reading a caller-supplied forwarding header.
Circuits are independent for Spend registry, authorization, management, administrator, and each NRS state. The default policy opens after at least 20 eligible calls have a failure ratio of 50% or more in 30 seconds, remains open for 30 seconds, and admits one half-open probe. Timeouts, transport errors, 5xx responses, rejected deployment-owned credentials, and invalid dependency contracts are failures. Customer 4xx or business rejections do not count.
When a circuit or bulkhead rejects work, honor Retry-After and retain the logical operation's idempotency key. Do not bypass the protection by switching hosts, replicas, credentials, or mutation keys.
Routing-loop response
Gateway rejects startup configuration when a Spend or NRS origin resolves to a configured Gateway public origin or the invoice-domain suffix. Network egress allowlists remain the primary control.
Every approved Gateway outbound dependency request carries the reserved X-Duplo-Gateway-Hop: 1 marker. Any request returning to Gateway with a non-zero marker is rejected immediately with 508 GW_ROUTING_LOOP_DETECTED, logged, and alerted. Customers must not send or copy this header.
For a loop incident:
- Stop retrying the affected route.
- Identify the configured upstream from the dependency event; never follow arbitrary request-supplied URLs.
- Verify DNS, reverse-proxy routes, service discovery, and environment-specific base URLs.
- Confirm the target is an approved Spend, administrator, or NRS origin rather than a Gateway public or service address.
- Correct configuration and egress policy, restart safely, then prove one read-only request before restoring mutation traffic.
NRS contract versioning
NRS behavior is isolated behind an explicit, state-specific adapter. The current supported contract is v1; TEST and LIVE select their versions independently. An unsupported version prevents the affected integration from starting or serving rather than silently guessing a schema.
Gateway preserves the status and bytes of ordinary successful NRS relay responses after safe-header filtering, so additive response fields remain compatible. It validates only fields needed for a Gateway decision, including onboarding authentication and entity selection, sign success, and IRN/business ownership. A sign is successful only when HTTP is 2xx, body code is 201, and data.ok is true; a malformed, empty, renamed, or otherwise unrecognized success envelope remains an ambiguous sign outcome.
A new NRS path, method, API version, credential header, or redirect requires a new Gateway adapter/route release. Redirects remain disabled. Deploy a new adapter alongside the existing one, prove golden fixtures and the NRS sandbox in TEST, then canary LIVE before changing the whole state. Credential rotation may stage current and next values, but a mutation must never be retried automatically with a second credential after an uncertain first dispatch.
Identity responses are durable snapshots. Additive name, template, or active-state drift can be reviewed; NRS business-ID or TIN drift requires an audited migration. Never silently rewrite the local identity or break a previously published public identifier alias.
Idempotency and safe retry rules
| Outcome | Required action |
|---|---|
| Read-only request fails before a response | At most one bounded, jittered retry within the original deadline, when that route documents retries as safe. |
408 GW_REQUEST_BODY_TIMEOUT | Send the complete body promptly with the same key and identical logical content. |
429 GW_RATE_LIMIT_EXCEEDED or 503 GW_GATEWAY_CAPACITY_EXCEEDED | Honor Retry-After; back off and preserve the same mutation key. |
| Same key with different content | Stop on 409 GW_IDEMPOTENCY_CONTENT_CONFLICT; never mutate the payload to bypass the record. |
| Sign, debit, reversal, or reconciliation has an uncertain outcome | Never replay automatically. Work the original operation and recovery record. |
Pass-through transmission is claimed | Only the holder of that durable claim may call NRS. Record the typed provider outcome afterward. |
Transmission claim is in_progress, succeeded, or rejected | Return GW_TRANSMIT_ATTEMPT_ALREADY_PROCESSED with the existing operationId; do not dispatch another NRS transmission. |
Transmission claim is unknown, or NRS dispatch cannot be classified safely | Return GW_TRANSMIT_OUTCOME_UNCONFIRMED; do not auto-replay. Reconcile by operationId. |
The current Pass-through transmission version accepts a missing idempotency key for compatibility and derives a legacy operation identity from the request binding. New mutation versions can require Idempotency-Key. Absence of a key never makes an uncertain external mutation safe to repeat.
Slack alert delivery
Gateway sends alerts directly to the configured Slack HTTPS webhook; it does not use the Slack SDK, Alertmanager, or a cloud monitoring delivery service. Request handlers publish a small allowlisted FailureSignal to a Redis stream and return without waiting for Slack. A background worker aggregates across replicas by environment, dependency, operation, and failure class.
The default sustained-failure trigger is either five consecutive failures or at least 20 calls in five minutes with a failure ratio above 20%. A 15-minute cooldown deduplicates an active incident. Five consecutive successes send one recovery notification.
Immediate signals bypass the sustained threshold for rejected internal service/provider credentials, unconfirmed billed sign or reversal work, routing loops, capacity/circuit safety incidents, missing post-sign recovery state, and overdue reconciliation work. Slack delivery has a three-second timeout and up to five exponential-backoff attempts. Exhausted deliveries go to the alert dead-letter stream and emit slack_alert_delivery_failed; Slack failures never recursively alert Slack.
An alert contains only allowlisted context: environment, dependency, operation, normalized failure class, first/last occurrence, count and ratio, up to three request IDs, optional operation/provider IDs, public error code, and configured runbook/log-search links. If Redis is unavailable, an immediate or Redis-related incident can use a per-pod best-effort direct webhook with the same 15-minute cooldown.
Incident procedures
Spend registry or authorization
- Confirm whether the public code is registry or authorization unavailable; a warm domain cache never permits stale authorization.
- Search the request ID across Gateway and Spend, then inspect contract version and deployed service credential.
- Confirm NRS received no request.
- Restore Spend, close the incident only after five successful observed calls, and let callers retry according to their Problem Details.
Administrator and billing recovery
- Before debit: confirm NRS was not called and instruct a same-key retry only when the response permits it.
- After debit: inspect the billing attempt, reversal queue, reconciliation marker, and operation ID. Never issue a manual compensating debit or replay NRS from the customer request.
- Escalate immediately when a reversal or reconciliation result cannot be confirmed.
NRS outage or contract drift
- Separate throttling, timeout, transport/5xx, provider-credential rejection, and invalid-contract responses.
- Compare the active TEST/LIVE adapter version and provider correlation ID with the last known good call.
- For ordinary reads, restore service or allow bounded retries. For sign/transmit, reconcile the original operation before any new dispatch.
- If a payload field or success envelope changed, capture a sanitized fixture, update the versioned adapter and golden tests, test in the NRS sandbox, then canary LIVE.
Stored processor backlog
- Treat
202 Acceptedas durable intake acknowledgement, not sign success. - Use Spend's processor heartbeat, oldest queue age, and invoice state. Infrastructure outages stay pending/validated and use dependency backoff; they must not consume an invoice's business retry budget or terminally reject a valid invoice.
- Spend, not Gateway, owns alerts occurring after the synchronous
202response.
Ambiguous sign or transmission
- Freeze automatic mutation retries for the IRN and represented business.
- Preserve request/operation IDs, the original idempotency key, request hash or exact bytes, target state, and UTC time.
- Inspect authoritative NRS state plus the Gateway/Spend recovery record. A missing optional NRS field is not proof that nothing committed.
- Resolve billing and provider state before allowing a deliberate new attempt.
Before closing an incident
- The dependency is healthy through the same contract version and credentials used by production traffic.
- Circuits have closed through a successful half-open probe; queues and recovery work are within their service-level objective.
- A safe read and, when authorized, one controlled idempotent workflow succeed end to end.
- No customer was instructed to replay an unconfirmed mutation.
- Slack emitted one recovery notification after the configured success threshold.
- The incident record contains request IDs and operation IDs, not secrets or raw invoice data.
How is this guide?