Symptoms
- Requests fail with
429 Too Many Requestsfrom*.vault.azure.net, often with aRetry-Afterheader. - Startup storms: scaling out from 10 to 100 instances, or rolling a large AKS deployment, makes every pod fail to start.
- Cryptographic operations (sign, wrap, decrypt) slow down during batch jobs.
Key Vault limits requests per vault, per region, over a 10-second window, with separate limits per operation type (secrets, keys by type, management). The exact numbers are in the "Azure Key Vault service limits" page; check them for your key types rather than assuming.
Step 1: confirm the throttling
Metrics (enable nothing extra):
KV_ID=$(az keyvault show -n kv-app-prod --query id -o tsv)
az monitor metrics list --resource $KV_ID --metric ServiceApiResult \
--filter "StatusCode eq '429'" --interval PT1M --aggregation Count \
--start-time 2026-10-28T08:00:00Z --end-time 2026-10-28T10:00:00Z -o table
az monitor metrics list --resource $KV_ID --metric ServiceApiHit \
--filter "ActivityName eq '*'" --interval PT1M --aggregation Count -o table
Spikes in ServiceApiHit that line up with 429s tell you when and which operations.
Step 2: find who is calling
With diagnostic settings sending AuditEvent logs to Log Analytics:
// Resource-specific table
AZKVAuditLogs
| where TimeGenerated > ago(4h)
| summarize calls = count(), throttled = countif(HttpStatusCode == 429)
by CallerIpAddress, Identity = tostring(Identity), OperationName, bin(TimeGenerated, 1m)
| order by calls desc
If your workspace uses the legacy AzureDiagnostics table:
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT" and TimeGenerated > ago(4h)
| summarize calls = count(), throttled = countif(httpStatusCode_d == 429)
by CallerIPAddress, OperationName, bin(TimeGenerated, 1m)
| order by calls desc
Column names differ between the two tables; check one row first. Typically you'll find one or two identities producing most calls, usually SecretGet in a tight loop.
Step 3: fix the callers
Cache secrets in the application
The most common cause is code that reads a secret per HTTP request or per message. Load once, cache in memory, refresh on a timer or on authentication failure.
// .NET: load secrets into configuration once, refresh periodically
builder.Configuration.AddAzureKeyVault(
new Uri("https://kv-app-prod.vault.azure.net/"),
new DefaultAzureCredential(),
new AzureKeyVaultConfigurationOptions { ReloadInterval = TimeSpan.FromMinutes(30) });
# Python: simple TTL cache around the SDK
import time
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
client = SecretClient("https://kv-app-prod.vault.azure.net/", DefaultAzureCredential())
_cache = {}
def get_secret(name, ttl=1800):
v = _cache.get(name)
if v and time.time() - v[1] < ttl:
return v[0]
value = client.get_secret(name).value
_cache[name] = (value, time.time())
return value
Respect Retry-After
The Azure SDKs retry 429s with exponential backoff by default. Hand-written REST clients often retry immediately, which makes throttling worse. Honour Retry-After and add jitter.
Platform features already cache for you
| Feature | Behaviour | Tip |
|---|---|---|
| App Service / Functions Key Vault references | Fetched at startup and refreshed periodically, not per request | Don't also read the same secrets in code |
| AKS Secrets Store CSI driver | Fetches at pod mount; optional rotation polling | Keep rotation-poll-interval in minutes, not seconds; large deployments mount many pods at once |
Azure SDK CryptographyClient | Can cache public key and do encrypt/verify locally | Only private-key operations need the vault |
Spread startup storms
- Use
maxSurgeandmaxUnavailablein AKS rollouts so 500 pods don't mount secrets at the same second. - Scale out in steps (autoscale rules with cooldown) for App Service.
Step 4: fix the design if one vault is shared by everyone
| Situation | Change |
|---|---|
| Many apps share one vault | One vault per application per environment per region |
| Multi-region app reading a vault in one region | A vault per region (limits are per region) |
| Heavy encrypt/decrypt in batch jobs | Envelope encryption: get a data key once, encrypt locally, only wrap/unwrap the data key in Key Vault |
| Very high crypto throughput or dedicated HSM needs | Evaluate Managed HSM, which has its own, separate limits |
Tip
Splitting vaults is cheap: Key Vault is billed per operation, not per vault. It also limits the blast radius of an access-policy or RBAC mistake.
Step 5: alert before users notice
az monitor metrics alert create -g rg-monitor -n kv-app-prod-429 \
--scopes $KV_ID --condition "total ServiceApiResult > 50 where StatusCode includes 429" \
--window-size 5m --evaluation-frequency 1m --severity 2
Also alert on the SaturationShoebox (vault saturation percentage) metric where available; it shows how close you are to the limit before you hit it.
Prevent it next time
- Code review rule: no Key Vault calls on the request path.
- Load test with the real number of instances and watch
ServiceApiHit. - Keep diagnostic logs on every vault so you can always answer "who is calling".
- Design one vault per app, per environment, per region from the start.