Guardian's rate limiter is attached only to the axum HTTP router. The tonic gRPC server — same mutating operations, same public hostname, same ALB — has no rate limiting. A client that wants to exceed GUARDIAN_RATE_BURST_PER_SEC/GUARDIAN_RATE_PER_MIN only has to speak gRPC. Since gRPC is the primary transport for the Rust SDK and the benchmark harness, the default client path is the unthrottled one.
Proposed behavior
- Tonic-side layer sharing one RateLimitStore with HTTP, so the transports draw from a single budget rather than one each. The store and its check logic are already transport-agnostic; only the layer is HTTP-bound (Service<Requestaxum::body::Body>, rate_limit.rs:350).
- Reuse the keying scheme: gRPC metadata are HTTP/2 headers, so x-pubkey enhanced keying works as-is, and the gRPC path gives per-method burst keys for free. Only the account_id query-param branch (rate_limit.rs:451-457) has no analogue.
- Reject with ResourceExhausted via the existing mapping — no new code, and retryable() already returns true (error.rs:384).
- Retry hint in Status metadata (no Retry-After on gRPC) — decide retry-after key vs. google.rpc.RetryInfo details.
- Same rejection counter, with a transport label.
Acceptance criteria
Guardian's rate limiter is attached only to the axum HTTP router. The tonic gRPC server — same mutating operations, same public hostname, same ALB — has no rate limiting. A client that wants to exceed GUARDIAN_RATE_BURST_PER_SEC/GUARDIAN_RATE_PER_MIN only has to speak gRPC. Since gRPC is the primary transport for the Rust SDK and the benchmark harness, the default client path is the unthrottled one.
Proposed behavior
Acceptance criteria