See also (wiki): ai-gateway · llm-cost-attribution · agentic-token-governance · enterprise-agent-runtime-infrastructure
LiteLLM is an open-source proxy/SDK providing a unified OpenAI-compatible API across 100+ providers (Anthropic, OpenAI, Azure, Bedrock, Vertex, Cohere, etc.). The proxy layer adds ~3.25ms overhead per call and benchmarks at 4,200+ requests per minute on a modest 4-core instance. This document covers production-grade deployment patterns, enterprise features, observability realities, and known failure modes.
The canonical starting point. Use the -database image variant when you need virtual keys, spend tracking, or the admin UI — it bundles PostgreSQL migrations.
docker pull docker.litellm.ai/berriai/litellm-database:main-stable
docker run \
-v $(pwd)/litellm_config.yaml:/app/config.yaml \
-e LITELLM_MASTER_KEY=sk-my-master-key \
-e DATABASE_URL=postgresql://user:pass@postgres:5432/litellm \
-e REDIS_URL=redis://redis:6379 \
-e LITELLM_SALT_KEY=<random-32-char-string> \
-p 4000:4000 \
docker.litellm.ai/berriai/litellm-database:main-stable \
--config /app/config.yaml
Key environment variables:
| Variable | Purpose |
|---|---|
LITELLM_MASTER_KEY |
Admin credential; must start with sk- |
DATABASE_URL |
PostgreSQL for keys, spend, teams |
LITELLM_SALT_KEY |
Encrypts stored provider API keys at rest — set before adding any model |
REDIS_URL |
Rate limiting and cross-instance router state |
LITELLM_MODE=PRODUCTION |
Disables .env file loading |
LITELLM_LOCAL_MODEL_COST_MAP=true |
Skip live pricing pull on startup (eliminates one cold-start network call) |
Minimal litellm_config.yaml:
model_list:
- model_name: gpt-4o
litellm_params:
model: azure/gpt-4o-prod
api_base: os.environ/AZURE_API_BASE
api_key: os.environ/AZURE_API_KEY
rpm: 300
general_settings:
database_connection_pool_limit: 10
allow_requests_on_db_unavailable: true
router_settings:
redis_host: os.environ/REDIS_HOST
redis_port: 6379
redis_password: os.environ/REDIS_PASSWORD
routing_strategy: simple-shuffle # avoid usage-based in prod — ~80 RPS slower
Health endpoints: GET /health/liveliness and GET /health/readiness on port 4000. Wire both into load balancer checks.
Horizontal scaling: Run multiple containers behind a load balancer. Each container needs REDIS_URL set — Redis is the shared state for rate limiting and router metrics. Without it, per-key rate limits are per-instance only.
2. Kubernetes / Helm
2.1 Official Helm Chart
LiteLLM ships an official Helm chart via OCI registry at docker.litellm.ai/berriai/litellm-helm. As of June 2026 the chart is at approximately v1.86–1.89 (mirrors the application version). It is not listed on ArtifactHub (open feature request: GitHub issue #12420).
helm pull oci://docker.litellm.ai/berriai/litellm-helm --version 1.86.2
helm install litellm ./litellm-helm -f values-prod.yaml
HA features the official chart supports:
| Feature | Default | Notes |
|---|---|---|
replicaCount |
1 | Must be raised manually |
| HPA (CPU) | disabled | autoscaling.enabled: true, targetCPUUtilizationPercentage: 80 |
| HPA (memory) | commented out | Must uncomment and set threshold |
| KEDA autoscaling | disabled | Prometheus-based; mutually exclusive with HPA |
| PDB | disabled | Supports both minAvailable and maxUnavailable |
| Topology spread | empty | Example commented in values.yaml for multi-zone |
terminationGracePeriodSeconds |
90 | Too low — must raise to 620+ (see below) |
| Redis | disabled | Optional caching; standalone architecture only |
| PostgreSQL | Bitnami standalone | No native HA postgres dependency |
| Resource limits | none | No defaults set; must specify explicitly |
Critical chart gap — terminationGracePeriodSeconds: The official chart defaults to 90 seconds, but LiteLLM’s default request timeout is 600 seconds. Pods will be killed mid-request during rolling updates unless you override:
# values-prod.yaml
terminationGracePeriodSeconds: 620
HA production values overlay:
replicaCount: 3
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 60 # docs recommend 60% CPU, 80% memory
podDisruptionBudget:
enabled: true
minAvailable: 2
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: litellm
terminationGracePeriodSeconds: 620
resources:
requests:
cpu: "1"
memory: "4Gi"
limits:
cpu: "2"
memory: "4Gi" # set limits == requests for predictable scheduling
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
readinessProbe:
httpGet:
path: /health/readiness
port: 4000
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/liveliness
port: 4000
initialDelaySeconds: 120 # DB migrations run on startup
periodSeconds: 15
failureThreshold: 3
2.2 Community / Third-Party Helm Charts
RichardoC/litellm_helm-chart — the most active community fork.
- ArtifactHub:
litellm-helm/litellm(latest: v1.89.0 as of mid-2026; also published atoci://ghcr.io/richardoc/litellm_helm-chart/litellm) - Adds: progressive rollout support (Argo Rollouts-compatible), PDB enabled by default, draws patterns from Argo Helm and Bitnami
- Install:
helm repo add litellm-helm https://RichardoC.github.io/litellm_helm-chart/ - Maturity: small single-maintainer project; tracks upstream application versions closely but lacks enterprise support
Unique-AG litellm chart (unique/litellm on ArtifactHub) — now deprecated. Do not use.
Official experimental microservices chart (docs.litellm.ai/docs/proxy/microservices_helm) — deploys four independent services with per-service HPA:
| Service | Min→Max Replicas | HPA CPU Target | Purpose |
|---|---|---|---|
| Gateway | 1→10 | 70% | LLM inference plane /chat/completions |
| Backend | 1→4 | 70% | Key/user/spend management API |
| UI | 1→3 | disabled | Next.js admin dashboard |
| Migrations | job | — | One-shot Prisma schema setup |
This chart is experimental — values schema may change between releases; pin the version and review diffs before upgrading. Architectural benefit: a load spike on the inference plane does not affect dashboard performance, and dashboard query slowness does not consume inference threads.
2.3 HA Architecture: Redis
Redis is required for accurate cross-pod rate limiting. Without it, per-key RPM/TPM limits are enforced per-pod, not globally.
LiteLLM supports three Redis topologies:
1. Standalone (default — not HA):
router_settings:
redis_host: redis.default.svc.cluster.local
redis_port: 6379
redis_password: os.environ/REDIS_PASSWORD
# NEVER use redis_url in K8s — ~80 RPS slower and breaks connection pooling
2. Redis Sentinel (recommended HA mode for most deployments):
litellm_settings:
cache: true
cache_params:
type: redis
sentinel_nodes: '[["redis-sentinel-0.redis-sentinel.default.svc", 26379], ["redis-sentinel-1.redis-sentinel.default.svc", 26379], ["redis-sentinel-2.redis-sentinel.default.svc", 26379]]'
service_name: "mymaster"
Or via env: REDIS_SENTINEL_NODES='[["host1", 26379], ["host2", 26379]]'
Deploy with Bitnami Redis chart in Sentinel mode:
helm install redis bitnami/redis \
--set sentinel.enabled=true \
--set sentinel.quorum=2 \
--set replica.replicaCount=3
3. Redis Cluster (for >2,000 QPS): Use redis_startup_nodes in cache_params with multiple shard node addresses, or use ElastiCache (cluster mode enabled) / Upstash.
HA-critical Redis settings in LiteLLM config:
general_settings:
use_redis_transaction_buffer: true # required for 10+ instances; buffers DB writes via Redis queue
use_shared_health_check: true # cross-pod health state shared via Redis
litellm_settings:
cache_params:
max_connections: 100 # fix for GitHub issue #15794 — async client ignored this before v1.77.7
enable_redis_auth_cache: true # caches virtual-key auth in Redis; reduces DB round trips
Redis circuit breaker (default-on since v1.82.0): Trips after 5 failures, fast-fails at 0ms for 60 seconds, then probes for recovery. Prevents the catastrophic pattern of 100+ pods each hanging 30 seconds on auth checks simultaneously.
REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD=5
REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT=60
2.4 HA Architecture: PostgreSQL
Connection budget formula:
total_connections = replicas × workers_per_pod × database_connection_pool_limit
Example: 5 pods × 1 worker × 10 pool limit = 50 connections. Standard RDS db.t3.medium allows ~170. Plan before scaling.
Recommended options by scale:
| Scale | PostgreSQL Option | Notes |
|---|---|---|
| Dev/staging | Bitnami PostgreSQL chart (single) | Simple; no HA |
| Production ≤500 RPS | CloudNativePG (CNCF sandbox, May 2025) | Declarative, streaming replication, built-in PgBouncer pooler |
| Production >500 RPS or AWS | RDS Aurora Multi-AZ + PgBouncer sidecar | IAM auth supported natively in LiteLLM Helm chart |
| Enterprise/regulated | CrunchyData PGO | PgBouncer native, pgBackRest, patroni failover |
PgBouncer (transaction mode) is required when running 5+ pods. Wire it between LiteLLM and PostgreSQL:
- LiteLLM sets
database_connection_pool_limitto the PgBouncer pool size, not the raw Postgres limit - Set
database_connection_pool_timeoutto detect pool exhaustion quickly rather than hanging - CloudNativePG and CrunchyData PGO both include integrated PgBouncer configuration
High-write-rate pattern (use_redis_transaction_buffer: true): Instead of every pod writing spend/rate-limit updates directly to PostgreSQL, all pods write to a Redis queue. A single elected pod (determined by Redis lock) drains the queue to Postgres in batches. Eliminates the deadlock pattern that occurs when 10+ pods simultaneously UPDATE the same user_id or team_id row.
DB connection pool settings in config:
general_settings:
database_connection_pool_limit: 10 # per worker; default 10
database_connection_pool_timeout: 30 # seconds before pool exhaustion error
proxy_batch_write_at: 60 # batch spend updates every 60s to reduce write pressure
allow_requests_on_db_unavailable: true # only safe in private VPC
2.5 Known Failure Modes (K8s-specific)
DB migration race condition on multi-pod startup (GitHub issue #16190, postmortem #22807):
- Root cause: the Helm chart migration job launches alongside the application pods without waiting for PostgreSQL readiness. When the job fails (DB not yet up), Kubernetes marks it “Completed” despite errors, so the tables are never created.
- Symptom from issue #22807: pod starts,
/healthreturns 200, but required tables are absent — no indication of failure. - Fix: add an
initContainerrunningprisma db push(orprisma migrate deploy) before the main container starts:
initContainers:
- name: db-migrate
image: docker.litellm.ai/berriai/litellm-database:main-stable
command: ["sh", "-c", "prisma db push --schema /app/schema.prisma"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: litellm-secrets
key: DATABASE_URL
Issue #16190 is closed as “not planned” — the official chart has not fixed this. Use the initContainer workaround.
Redis async client ignores max_connections (GitHub issue #15794, fixed in v1.77.7-stable via PR #15797):
- Root cause:
max_connectionsincache_paramswas added to kwargs but never passed to the async Redis client initialization, leaving connections unmanaged. - Symptom: “ERR max number of clients reached” under load; unbounded connection growth.
- Fix: upgrade to v1.77.7-stable or later; explicitly set
max_connections: 100incache_params.
False readiness on separate health app (GitHub issue #18415):
- Root cause: the health endpoint becomes ready in ~1 second while the main application needs ~10 seconds to fully initialize, and when Redis is down or the cache is unavailable the readiness probe can return healthy incorrectly.
- Fix: increase
initialDelaySecondson readiness probe to 30+, add Redis availability check if using shared health state.
readOnlyRootFilesystem permission error on migrations (GitHub issue #19859):
- Root cause:
prisma generatewrites files to the container filesystem; fails whenreadOnlyRootFilesystem: true. - Fix: add
emptyDirvolumes mounted at the Prisma output path and the UI assets directory.
Budget leak / phantom BudgetExceededError (GitHub issue #27639, affects v1.84.0):
- Root cause:
reserve_budget_for_request()leaks Redis spend counters when usinguse_redis_transaction_buffer. Phantom budget exceeded errors appear despite actual spend being below limit. - Status: reported June 2026; workaround is to restart pods to clear Redis counters.
OpenShift / HTTPS proxy migration failure (GitHub issue #7173):
- The migration Job pod cannot run on OpenShift (security context constraints) or behind an HTTPS proxy.
- Fix: use the initContainer pattern above instead of the separate migration job.
2.6 Secret Management
LiteLLM Helm chart values support three patterns in order of security preference:
1. External Secrets Operator (recommended for enterprise):
# ExternalSecret CR pointing to AWS Secrets Manager / Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: litellm-secrets
spec:
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: litellm-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: /litellm/prod/database-url
- secretKey: LITELLM_MASTER_KEY
remoteRef:
key: /litellm/prod/master-key
- secretKey: LITELLM_SALT_KEY
remoteRef:
key: /litellm/prod/salt-key
2. Vault Secrets Operator: Syncs Vault secrets to K8s secrets natively; preferred for existing Vault deployments.
3. Kubernetes Secrets (minimum viable): Mount as envFrom. Never store in litellm_config.yaml or values.yaml — reference via os.environ/VAR_NAME syntax in the config file.
Critical secret hygiene notes:
LITELLM_SALT_KEYmust be set before any model credentials are stored. Changing it after the fact requires re-encrypting all stored provider keys.- Master key bypasses all budget/rate limits — treat as a root credential, not an application key.
- Issue virtual keys per team/service with scoped model access and budget caps. Never share the master key with application code.
2.7 HA Control Plane (Beta / Enterprise)
The HA Control Plane is a separate enterprise feature (not the same as multi-replica deployment). It deploys a single admin UI managing multiple fully independent LiteLLM worker instances, each with its own database, Redis, and master key.
# Control plane config
general_settings:
worker_registry:
- worker_id: "worker-us-east"
name: "US East Worker"
url: "http://litellm-us-east:4000"
- worker_id: "worker-eu-west"
name: "EU West Worker"
url: "http://litellm-eu-west:4000"
Use case: regional data residency, blast-radius isolation across business units, or active-active multi-region deployments where a shared PostgreSQL/Redis would be a single point of failure.
2.8 Recommended HA Values Summary (Production Baseline)
# values-ha-prod.yaml — official chart
replicaCount: 3
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 60
podDisruptionBudget:
enabled: true
minAvailable: 2
terminationGracePeriodSeconds: 620
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
resources:
requests:
cpu: "1"
memory: "4Gi"
limits:
cpu: "2"
memory: "4Gi"
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 101
extraVolumes:
- name: tmp
emptyDir: {}
- name: prisma-cache
emptyDir: {}
extraVolumeMounts:
- name: tmp
mountPath: /tmp
- name: prisma-cache
mountPath: /app/.prisma
initContainers:
- name: db-migrate
image: docker.litellm.ai/berriai/litellm-database:main-stable
command: ["sh", "-c", "prisma migrate deploy --schema /app/schema.prisma"]
envFrom:
- secretRef:
name: litellm-secrets
env:
- name: LITELLM_MODE
value: "PRODUCTION"
- name: LITELLM_LOCAL_MODEL_COST_MAP
value: "true"
- name: REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD
value: "5"
- name: REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT
value: "60"
# proxy_config.yaml content (passed as configmap):
# general_settings:
# database_connection_pool_limit: 10
# database_connection_pool_timeout: 30
# proxy_batch_write_at: 60
# use_redis_transaction_buffer: true
# use_shared_health_check: true
# allow_requests_on_db_unavailable: true
#
# router_settings:
# redis_host: redis-sentinel.default.svc.cluster.local
# redis_port: 6379
# redis_password: os.environ/REDIS_PASSWORD
# routing_strategy: simple-shuffle
#
# litellm_settings:
# cache: true
# cache_params:
# type: redis
# sentinel_nodes: '[["redis-s-0:26379","redis-s-1:26379","redis-s-2:26379"]]'
# service_name: "mymaster"
# max_connections: 100
# set_verbose: false
# json_logs: true
Chart maturity assessment:
- Official chart (
docker.litellm.ai/berriai/litellm-helm): ships with every LiteLLM release, actively maintained, but has significant HA gaps out-of-the-box (terminationGracePeriodSeconds too low, no PDB, no initContainer for migrations, no resource defaults). Production use requires substantial values override. - RichardoC community chart: better production defaults (PDB on by default, progressive rollouts), but single-maintainer and lagging on the experimental microservices architecture.
- Experimental microservices chart: best isolation model for high-traffic production, but schema instability between releases makes it risky to adopt without version pinning.
Recommended chart selection:
- Teams with <500 RPS starting out: official chart with the HA values overlay above.
- Teams running >500 RPS or needing inference/management plane isolation: experimental microservices chart, pinned version.
- Teams on OpenShift or with progressive rollout requirements: RichardoC community chart.
3. Serverless / Managed
AWS ECS Fargate: No cold starts (tasks remain running); scaling new tasks takes 30–60 seconds. Official Terraform reference at github.com/BerriAI/litellm-ecs-deployment. Fargate is the better choice when consistent latency matters more than cost-at-zero-traffic.
GCP Cloud Run: Scales to zero, but cold starts add 8–15 seconds when a new instance initializes — the proxy must connect to PostgreSQL, run any pending migrations, and optionally pull model pricing. Mitigate with --min-instances=1. Appropriate for dev/staging or bursty batch workloads where cold starts are acceptable.
Railway / Render: Viable for smaller teams. Both support persistent containers (not function-as-a-service), so there are no cold start penalties. PostgreSQL and Redis add-ons available on both platforms. Not suitable for >500 RPS — managed Redis connection limits become the ceiling.
Cold start note: Disable live pricing pulls (LITELLM_LOCAL_MODEL_COST_MAP=true) in any environment where cold start time matters. The default behavior fetches a remote pricing JSON file on startup.
4. Enterprise Features
LiteLLM OSS covers the core gateway. Enterprise tier (commercial license) adds SSO (Okta, Azure AD, Google Workspace, OIDC/SAML), JWT auth, and role-based access controls.
Virtual keys: Generated at /key/generate, scoped to specific models, with hard budget caps:
curl http://localhost:4000/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{
"models": ["gpt-4o", "claude-3-5-sonnet"],
"max_budget": 50.00,
"budget_duration": "30d",
"rpm_limit": 100,
"tpm_limit": 500000
}'
Spend tracking: Automatic via completion_cost() on every response. Tracked at key / user / team level, queryable via /key/info, /user/info, /team/info. All spend aggregates to the LiteLLM_VerificationTokenTable in PostgreSQL.
Budget controls: max_budget (total cap), budget_duration (rolling window), temp_budget_increase (temporary overrides with expiry). Budget exhaustion returns a hard 429.
Rate limiting: rpm_limit, tpm_limit, and max_parallel_requests per key. Redis is required for accurate cross-instance enforcement — without it, limits are per-pod only.
Model fallbacks:
model_list:
- model_name: primary-gpt4
litellm_params:
model: azure/gpt-4o-eastus
- model_name: fallback-gpt4
litellm_params:
model: openai/gpt-4o
router_settings:
fallbacks: [{"primary-gpt4": ["fallback-gpt4"]}]
num_retries: 3
retry_after: 5
Load balancing: The routing_strategy options are simple-shuffle (recommended for production), least-busy, latency-based-routing (38% lower p95 vs. round-robin in multi-region deployments, but ~50ms overhead on cold proxy startup and ~5% extra memory for per-endpoint metrics).
Config reload: LiteLLM does not support zero-downtime config reload. Rolling restarts via Kubernetes RollingUpdate strategy (with maxUnavailable: 0) are the standard approach.
5. Observability Integration
Prometheus: /metrics endpoint exposed by default. Covers request count, latency histograms, error rates, and token usage per model.
OpenTelemetry: Set via environment:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
OTEL_SERVICE_NAME=litellm-proxy
What IS captured in OTEL traces:
gen_ai.usage.input_tokens,gen_ai.usage.output_tokens,gen_ai.usage.total_tokensgen_ai.client.response.time_to_first_token(streaming only)gen_ai.client.response.duration(vendor latency, excludes LiteLLM overhead)gen_ai.client.operation.duration(end-to-end including proxy overhead)gen_ai.cost.total_cost,gen_ai.cost.input_cost,gen_ai.cost.output_cost- Model name, provider, operation type, team/org/key metadata
- Prompt and completion content via
gen_ai.input.messages/gen_ai.output.messages— only whenOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT
What is NOT captured by default: Response content is suppressed unless CAPTURE_MESSAGE_CONTENT is explicitly set. Raw provider request/response body attributes (llm.{provider}.*) are suppressed in semconv mode. Per-turn token usage for multi-turn agent trajectories lives only in AgentResult.trajectory and must be manually correlated with proxy logs by timestamp.
Callback integrations: Langfuse, Helicone, Arize, MLflow, PromptLayer, and Splunk are supported as pluggable callbacks. Configuration in litellm_config.yaml:
litellm_settings:
success_callback: ["langfuse"]
failure_callback: ["langfuse"]
6. Security
Master key vs. virtual keys: The master key is a superadmin credential — it bypasses all budget and rate limits. Never expose it to application code. Issue virtual keys with scoped model access and budget caps for every consumer.
Network isolation: Run the proxy in a private VPC with no public endpoint. Expose only via an internal load balancer or service mesh. The only inbound port needed is 4000.
Secret rotation: Use LITELLM_KEY_ROTATION_ENABLED=true with a grace_period (e.g., "24h") so old and new keys overlap during rotation. Set LITELLM_SALT_KEY once at initialization — changing it after models are stored requires re-encrypting all stored credentials.
Audit logging: Enable json_logs: true and disable verbose mode (set_verbose: False) in production. Log ingestion (Splunk, Datadog, CloudWatch) should consume stdout JSON for audit trails.
Container hardening: readOnlyRootFilesystem: true is fully supported. Add emptyDir volumes for /tmp and the UI assets directory.
7. Performance Benchmarks
| Metric | Value |
|---|---|
| Proxy overhead (p50) | ~3.25ms vs. direct API call |
| Throughput (4-core instance) | 4,200+ RPM |
| Latency-based routing p95 improvement | 38% lower vs. round-robin |
Redis redis_url vs. separate params |
~80 RPS slower with redis_url |
| Recommended minimum spec (production) | 4 CPU, 8 GB RAM |
| High-traffic spec (1,000+ RPS) | Redis required; managed cluster (ElastiCache/Upstash) beyond 2,000 QPS |
8. Known Production Failure Modes
K8s-specific failure modes with GitHub issue references are detailed in §2.5. Summary of all failure modes:
Database connection exhaustion: The most common production incident. Formula: replicas × workers × database_connection_pool_limit. Standard RDS db.t3.medium max is ~170. Deploy PgBouncer in transaction mode for 5+ pods. Set database_connection_pool_timeout: 30 to surface exhaustion quickly rather than hanging silently.
Redis connection pool exhaustion (GitHub issue #15794, fixed v1.77.7): Triggers “ERR max number of clients reached” under ~300 concurrent requests with default config because the async client ignored max_connections. Set max_connections: 100 in cache_params and upgrade to v1.77.7+. Move to Redis Sentinel or ElastiCache cluster mode beyond 2,000 QPS.
Slow Redis more dangerous than dead Redis: 100+ pods hanging 30 seconds on auth checks fill threadpools silently, cascading into PostgreSQL. Circuit breaker (default-on since v1.82.0) resolves this — tune REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD=5 and REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT=60.
DB migration race condition on pod startup (GitHub issue #16190, #22807): Migration job marks “Completed” even when Postgres wasn’t ready; missing tables are invisible at startup because /health still returns 200. Use the initContainer workaround in §2.5 — the official chart has not fixed this (issue closed as “not planned”).
Phantom budget exceeded errors (GitHub issue #27639, v1.84.0): Redis spend counters leak when using use_redis_transaction_buffer. Pods report budget exceeded despite actual spend being below limit. Workaround: rolling pod restart clears counters.
Config reload requires restart: No hot reload. Rolling restarts are necessary for model list or routing changes. Use maxUnavailable: 0 and maxSurge: 1 with terminationGracePeriodSeconds: 620+.
Worker memory growth: Under sustained load, Uvicorn workers accumulate memory. Set MAX_REQUESTS_BEFORE_RESTART to recycle workers after a fixed request count (e.g., 10,000) before RSS grows unbounded.
Decision Matrix
| Deployment | Best For | Hard Limit |
|---|---|---|
| Docker Compose | Single-team internal gateway, dev/staging | No HA; single failure domain |
| Kubernetes + Helm | Multi-team enterprise, >100 RPS | Operational complexity |
| AWS ECS Fargate | AWS-native shops, latency-sensitive | 30–60s scale-out lag |
| GCP Cloud Run | Bursty/batch, cost optimization | 8–15s cold start without min-instances |
| Railway / Render | Startups, <500 RPS | Managed Redis connection ceiling |
For Fortune 500 deployments: Kubernetes with the official Helm chart, PostgreSQL with PgBouncer, managed Redis (ElastiCache/Upstash), virtual keys per team, and OTEL to your existing observability stack. Enable OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_AND_EVENT only if your data governance policy permits storing prompt/completion content in traces.
9. AWS Bedrock Cost Attribution via Inference Profiles
9.1 The Three Profile Types
AWS Bedrock has two conceptually distinct profile types, plus a third “global” variant:
| Profile Type | Who Creates It | Purpose | ARN Prefix |
|---|---|---|---|
| System inference profile (cross-region) | AWS (predefined) | Automatic geographic load balancing across regions in a named zone (US, EU, AP, JP) | arn:aws:bedrock:<region>::inference-profile/<zone>.<model-id> — note double-colon, no account ID |
| Application inference profile (AIP) | You (per account) | Cost attribution via AWS cost allocation tags; wraps any foundation model or system profile | arn:aws:bedrock:<region>:<account-id>:application-inference-profile/<id> |
| Global inference profile | AWS (predefined) | Routes to optimal commercial region globally; newer variant of cross-region | arn:aws:bedrock:<region>:<account-id>:inference-profile/global.<model-id> |
Key distinction for enterprises: System/global inference profiles solve throughput and latency (request goes to least-loaded region). Application inference profiles solve billing (request shows up in Cost Explorer under your tag). You can nest them: create an AIP whose source model is a system inference profile to get both geographic load balancing and cost attribution.
For LiteLLM routing to Bedrock, the ARN you pass in config is always the application inference profile ARN (application-inference-profile/...). Do not pass the system profile ARN directly if you need cost tags — system profiles have no tagging mechanism.
9.2 ARN Formats
# System (cross-region) inference profile — AWS managed, no account ID
arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0
# Application inference profile — customer-created, has account ID
arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6
# Global inference profile — AWS managed, routes to best worldwide region
arn:aws:bedrock:us-east-2:111122223333:inference-profile/global.anthropic.claude-sonnet-4-20250514-v1:0
Model request ID (not ARN): The application-inference-profile/<id> suffix is an opaque alphanumeric identifier returned in the inferenceProfileArn field when you create the profile. It is not the model ID or a human-readable name.
9.3 Creating Application Inference Profiles with Cost Allocation Tags
# Step 1: Create the profile, wrapping a foundation model or system profile
aws bedrock create-inference-profile \
--inference-profile-name "fraud-detection-prod" \
--description "Fraud detection team production workload" \
--model-source 'copyFrom=arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0' \
--tags '[
{"key":"Project","value":"fraud-detection"},
{"key":"Team","value":"data-science"},
{"key":"CostCenter","value":"CC-8821"},
{"key":"Environment","value":"production"}
]' \
--region us-east-1
# Output:
# {
# "inferenceProfileArn": "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6",
# "status": "ACTIVE"
# }
# Step 2: Add additional tags post-creation if needed
aws bedrock tag-resource \
--resource-arn arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6 \
--tags key=Owner,value=alice@example.com
# Step 3: Verify
aws bedrock list-inference-profiles --type-equals APPLICATION --region us-east-1
# Step 4: Activate tags as cost allocation tags in Billing console
# (Manual step — AWS Console → Billing → Cost allocation tags → activate)
# Tags take up to 24 hours to appear in Cost Explorer after activation.
# Cost allocation tags are NOT retroactive — only future costs are tagged.
To wrap a cross-region system profile instead (get both load balancing + cost attribution):
aws bedrock create-inference-profile \
--inference-profile-name "fraud-detection-multi-region" \
--model-source 'copyFrom=arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0' \
--tags '[{"key":"Team","value":"data-science"}]' \
--region us-east-1
Profile proliferation caveat: Each AIP is model-specific. One AIP per model per tag combination. For 10 teams × 5 models = 50 profiles minimum. AWS recommends tagging at the team or cost-center level (not per-user) to limit proliferation. For per-user attribution without profiles, use IAM principal tracking (separate feature in CUR 2.0 that attributes by IAM role/user automatically).
9.4 LiteLLM Configuration for Bedrock Inference Profiles
Working config as of LiteLLM v1.79+:
model_list:
# Application Inference Profile via model_id parameter (RECOMMENDED — works reliably)
- model_name: claude-fraud-detection
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0 # base model name tells LiteLLM the provider
model_id: arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6
aws_region_name: us-east-1
# Alternative: explicit converse route with ARN in model string
- model_name: claude-fraud-detection-alt
litellm_params:
model: bedrock/converse/arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6
aws_region_name: us-east-1
# Cross-region system inference profile (load balancing, no cost tags)
- model_name: claude-us-distributed
litellm_params:
model: bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0
aws_region_name: us-east-1
Do NOT use this (broken as of GitHub issue #18258, filed Dec 2025, not yet fixed):
# BROKEN — fails with "Unknown provider=None"
model: bedrock/arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6
Root cause (GitHub issue #18258): LiteLLM’s parser extracts the provider (Anthropic, Amazon, etc.) from the model string. Application inference profile ARNs contain no provider information, so provider=None and the request fails. The model_id workaround routes the call to the AIP while the base model string provides provider identification.
GitHub issue #8911 (older report, also unfixed as of mid-2026) — same class of bug for image generation models with AIPs.
Python SDK equivalent:
from litellm import completion
response = completion(
model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
model_id="arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/a1b2c3d4e5f6",
messages=[{"role": "user", "content": "Analyze this transaction for fraud."}],
aws_region_name="us-east-1"
)
Multi-team setup pattern: Create one LiteLLM virtual key per team, one AIP per team per model. Map each virtual key to the model alias that points to the team’s AIP:
model_list:
- model_name: claude-team-fraud
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
model_id: arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/fraud-team-profile-id
- model_name: claude-team-riskops
litellm_params:
model: bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0
model_id: arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/riskops-team-profile-id
Then issue virtual keys scoped to models: ["claude-team-fraud"] for the fraud team, models: ["claude-team-riskops"] for risk ops. AWS billing shows per-team spend via AIP tags; LiteLLM virtual key tracking provides the spend view within the proxy UI.
9.5 IAM Policy for Application Inference Profiles
The LiteLLM pod’s IAM role (or the credentials it uses) needs these permissions. The official AWS prerequisite policy (from docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeViaProfile",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:CreateInferenceProfile"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:*:*:inference-profile/*",
"arn:aws:bedrock:*:*:application-inference-profile/*"
]
},
{
"Sid": "ManageProfiles",
"Effect": "Allow",
"Action": [
"bedrock:GetInferenceProfile",
"bedrock:ListInferenceProfiles",
"bedrock:DeleteInferenceProfile",
"bedrock:TagResource",
"bedrock:UntagResource",
"bedrock:ListTagsForResource"
],
"Resource": [
"arn:aws:bedrock:*:*:inference-profile/*",
"arn:aws:bedrock:*:*:application-inference-profile/*"
]
}
]
}
Least-privilege hardening for LiteLLM runtime (inference only, no profile creation/deletion from the app pod):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeViaNamedProfiles",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:us-east-1::foundation-model/*",
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/*"
]
},
{
"Sid": "DenyDirectModelAccess",
"Effect": "Deny",
"Action": ["bedrock:InvokeModel*"],
"Resource": "arn:aws:bedrock:*::foundation-model/*",
"Condition": {
"Null": {
"bedrock:InferenceProfileArn": "true"
}
}
}
]
}
The Deny statement with bedrock:InferenceProfileArn null condition forces all model invocations through an inference profile — direct model calls without a profile ARN are blocked. This is useful when you want to guarantee every Bedrock call is tagged for cost attribution.
Profile creation should be in a separate admin role, not the LiteLLM runtime role. Create AIPs via CI/CD or Terraform, not at application runtime.
Terraform resource (if managing profiles as IaC):
resource "aws_bedrock_inference_profile" "fraud_detection" {
name = "fraud-detection-prod"
description = "Fraud detection team Claude 3.5 Sonnet"
model_source {
copy_from = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0"
}
tags = {
Project = "fraud-detection"
Team = "data-science"
CostCenter = "CC-8821"
Environment = "production"
}
}
9.6 Cost Attribution Flow to Cost Explorer
- Create AIP with tags → tags are on the AIP resource
- Activate those tag keys as cost allocation tags in AWS Billing console (one-time, per tag key)
- 24-hour propagation delay before tags appear in Cost Explorer
- In Cost Explorer: Group by tag (e.g.,
Project) → see aggregated daily USD spend per project - In CUR 2.0: query
resource_tags/user_Projectcolumn for line-item detail
Granularity ceiling: AIPs deliver aggregated billed dollars per usage type per day. They do not produce per-request cost. For per-prompt token counts, use LiteLLM’s own LiteLLM_SpendLogs table or enable Bedrock model invocation logging to S3 + Athena.
10. Day-2 Operations
10.1 Secret Rotation Without Downtime
Rotating LiteLLM virtual keys (automated):
# Enable in env
LITELLM_KEY_ROTATION_ENABLED=true
LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS=86400 # default: 24h
# Per-key grace period — old and new key both valid during overlap
curl http://localhost:4000/key/regenerate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{"key": "sk-old-key", "grace_period": "24h"}'
Rotating the master key (requires brief coordination):
- Back up PostgreSQL (
pg_dump). - POST to
/key/regeneratewith current master key and the new master key value — this re-encrypts all model credentials inProxy_ModelTable. - Update
LITELLM_MASTER_KEYenvironment variable (Kubernetes Secret or AWS Secrets Manager). - Rolling restart pods to pick up the new env var.
- Validate with a test request using the new key.
CRITICAL: LITELLM_SALT_KEY must never be rotated unless you are prepared to re-encrypt every stored provider credential. It is the encryption key for provider API keys in Proxy_ModelTable. If you must rotate it, decrypt all stored keys with the old salt first, update the salt, then re-encrypt.
Rotating the PostgreSQL password (DB credential rotation):
- Add the new password to AWS Secrets Manager (or Vault) alongside the old password.
- Update the External Secrets Operator
ExternalSecretto point to the new secret version. - Wait for ESO to sync the Kubernetes secret (typically <1 minute with
refreshInterval: 1m). - Rolling restart LiteLLM pods — each new pod picks up the refreshed
DATABASE_URL. - After all pods cycle and are healthy, revoke the old DB password.
The old pods continue running with the old password during the rolling update; new pods come up with the new password. This gives zero-downtime rotation if maxUnavailable: 0 is set.
Rotating Redis password: Same pattern as PostgreSQL — update the Kubernetes Secret, rolling restart pods. Redis Sentinel handles the password refresh transparently if you use Bitnami’s REDIS_MASTER_PASSWORD_FILE mechanism.
10.2 Upgrade Strategy
Version cadence: LiteLLM ships new releases several times per week. The main-stable tag (format: main-v<VERSION>-stable) is released weekly with a curated set of fixes. For production, pin to stable.
# In Helm values — pin the image tag explicitly
image:
tag: "main-v1.89.0-stable"
Never use latest or main in production — LiteLLM’s fast release cadence means main can include breaking changes within hours.
Upgrade sequence:
- Review the LiteLLM Release Notes for DB migration notes, breaking config changes, or known K8s issues.
- Back up PostgreSQL:
pg_dump -h <host> -U <user> -d litellm -F c -f litellm_backup_$(date +%Y%m%d_%H%M%S).dump - Upgrade Helm chart:
helm upgrade litellm ./litellm-helm --version <new-version> -f values-prod.yaml - The initContainer migration (
prisma migrate deploy) runs automatically before the main container starts. - Verify
/healthreturns 200; checkLiteLLM_SpendLogsis still populating.
Known upgrade hazard — September 6, 2025 release: Caused Out of Memory errors on Kubernetes. Pattern: fast-moving releases occasionally introduce memory regressions. Always stage upgrades in a non-production environment first.
Rollback procedure:
helm rollback litellm [revision-number]— reverts Helm chart to previous revision.- If DB migration applied new tables/columns that the old version doesn’t understand: connect to PostgreSQL and DELETE rows from
_prisma_migrationsthat belong to the version being rolled back. LiteLLM will re-apply them when you re-upgrade. LITELLM_SALT_KEYmust remain unchanged — do not swap it during rollback.- After rollback: validate with
GET /health,GET /health/readiness, and a test inference call.
10.3 Alerting Thresholds
Prometheus / AlertManager rules (recommended production baselines):
groups:
- name: litellm
rules:
# P95 latency — proxy overhead should be <50ms; >500ms indicates upstream provider issue
- alert: LiteLLMHighP95Latency
expr: histogram_quantile(0.95, rate(litellm_request_duration_seconds_bucket[5m])) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "LiteLLM p95 latency exceeds 500ms"
# Error rate — normal is <1%; >5% indicates provider degradation or config issue
- alert: LiteLLMHighErrorRate
expr: rate(litellm_requests_total{status="error"}[5m]) / rate(litellm_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
# Budget exhaustion — fire before teams hit their cap, not after
- alert: LiteLLMBudgetNearExhaustion
expr: litellm_team_budget_used_dollars / litellm_team_budget_limit_dollars > 0.85
for: 1m
labels:
severity: warning
annotations:
summary: "Team {{ $labels.team_id }} at 85% budget"
# Hanging requests (LiteLLM Enterprise PagerDuty integration)
# Configure in litellm_config.yaml:
# alerting_args:
# failure_threshold: 10
# failure_threshold_window_seconds: 60
# hanging_threshold_seconds: 30
# hanging_threshold_window_seconds: 60
# DB connection pool exhaustion
- alert: LiteLLMDBPoolExhaustion
expr: litellm_db_connection_pool_size - litellm_db_connection_pool_available < 2
for: 2m
labels:
severity: critical
# Redis circuit breaker tripped
- alert: LiteLLMRedisCircuitBreakerOpen
expr: litellm_redis_circuit_breaker_state == 1
for: 1m
labels:
severity: warning
Datadog / Grafana: LiteLLM’s official Grafana dashboard (ID 24055) covers request count, latency percentiles, error rates, and cache efficiency. Datadog has a native LiteLLM integration with APM tracing.
PagerDuty alerting (Enterprise only):
general_settings:
alerting: ["pagerduty"]
alerting_args:
failure_threshold: 10
failure_threshold_window_seconds: 60
hanging_threshold_seconds: 30
hanging_threshold_window_seconds: 60
Requires PAGERDUTY_API_KEY env var. Not available in OSS; 7-day Enterprise trial available.
10.4 Log Aggregation
What LiteLLM writes where:
| Data | Destination | Notes |
|---|---|---|
| Request/response HTTP logs | stdout (JSON) | Set json_logs: true; pipe to CloudWatch / Datadog / Splunk |
| Spend logs (token usage, cost, timing) | PostgreSQL LiteLLM_SpendLogs + stdout |
Disable with disable_spend_logs: True if DB write pressure is high |
| Error logs | stdout + PostgreSQL | Disable DB write with disable_error_logs: True in production |
| Audit log (config changes) | PostgreSQL LiteLLM_AuditLog |
Disabled by default; enable if compliance requires it |
| Callback data (Langfuse, Helicone, etc.) | External callback endpoint | Configured via success_callback/failure_callback |
Recommended production logging config:
litellm_settings:
set_verbose: false # suppress debug logs
json_logs: true # structured JSON to stdout
disable_error_logs: true # keep DB writes lean; errors go to stdout only
general_settings:
disable_spend_logs: false # keep spend logs in DB for UI; disable if >10K RPS
Log shipping: Use Fluent Bit / Fluentd as a DaemonSet sidecar to ship stdout JSON to CloudWatch Logs, Datadog, or Splunk. LiteLLM’s stdout format with json_logs: true is compatible with CloudWatch Insights queries without further parsing.
10.5 PostgreSQL Backup and Restore
What to back up:
| Table | Criticality | Notes |
|---|---|---|
LiteLLM_VerificationToken |
CRITICAL | Virtual keys, budgets, model access |
LiteLLM_TeamTable |
CRITICAL | Team budgets and rate limits |
LiteLLM_UserTable |
CRITICAL | User accounts |
LiteLLM_BudgetTable |
CRITICAL | Budget configurations |
Proxy_ModelTable |
CRITICAL | Encrypted provider API keys |
LiteLLM_SpendLogs |
HIGH | Historical spend; large; can be rebuilt from OTEL if needed |
LiteLLM_AuditLog |
MEDIUM | Compliance; disabled by default |
Backup commands:
# Full backup
pg_dump -h $DB_HOST -U $DB_USER -d litellm \
-F c -f litellm_backup_$(date +%Y%m%d_%H%M%S).dump
# Critical tables only (smaller, faster)
pg_dump -h $DB_HOST -U $DB_USER -d litellm \
-F c \
-t '"LiteLLM_VerificationToken"' \
-t '"LiteLLM_TeamTable"' \
-t '"LiteLLM_UserTable"' \
-t '"LiteLLM_BudgetTable"' \
-t '"Proxy_ModelTable"' \
-f litellm_critical_$(date +%Y%m%d).dump
# Restore
pg_restore -h $DB_HOST -U $DB_USER -d litellm --clean litellm_backup_20260616_120000.dump
For RDS: Use automated snapshots (RPO of 5 minutes with PITR). Supplement with a daily pg_dump to S3 for cross-region durability.
Spend logs size management: LiteLLM_SpendLogs grows at roughly 1–2 KB per request. At 1,000 RPS this is ~86 GB/day. Implement a partition or archival job:
-- Archive logs older than 90 days to separate table or drop
DELETE FROM "LiteLLM_SpendLogs"
WHERE "startTime" < NOW() - INTERVAL '90 days';
10.6 Multi-Region: Active-Active vs. Active-Passive
Active-passive (recommended starting point):
- Primary region: full LiteLLM stack (pods + PostgreSQL + Redis)
- Standby region: LiteLLM pods ready but routing 0% traffic; PostgreSQL read replica
- Failover: promote standby PostgreSQL to primary, update
DATABASE_URLin standby LiteLLM pods, shift DNS/ALB to standby region - RTO: 5–15 minutes (automated with Route 53 health checks)
- RPO: seconds (Aurora Global Database with 1-second replication lag)
Active-active (via LiteLLM HA Control Plane — Enterprise): LiteLLM’s HA Control Plane deploys independent worker instances per region, each with its own PostgreSQL and Redis, managed via a single admin UI:
# control-plane config
general_settings:
worker_registry:
- worker_id: "worker-us-east-1"
name: "US East"
url: "http://litellm-us-east.internal:4000"
- worker_id: "worker-eu-west-1"
name: "EU West"
url: "http://litellm-eu-west.internal:4000"
Each regional worker has full autonomy — no shared state between regions. This means:
- Rate limits are per-region (a team in US and EU have separate counters unless you enforce at the ALB level)
- Spend is per-region (aggregate in your BI/OTEL layer)
- Virtual keys must be issued per-region or replicated via your IaC tooling
AWS Bedrock cross-region note: When using Bedrock system inference profiles (e.g., us.anthropic.claude-3-5-sonnet-20241022-v2:0) from LiteLLM, Bedrock itself handles regional routing within the zone. This is separate from LiteLLM multi-region — LiteLLM can be in us-east-1 while Bedrock distributes across us-east-1, us-west-2, and us-west-1 transparently. For this reason, many enterprises run a single-region LiteLLM deployment and rely on Bedrock’s built-in cross-region inference for throughput, reserving LiteLLM multi-region only for data residency requirements.
Aurora Global Database is the recommended PostgreSQL backend for active-active, with one writer endpoint per region and sub-1-second replication. Wire each regional LiteLLM stack to its local Aurora endpoint to avoid cross-region write latency.
11. Bedrock Cost Attribution — All Mechanisms
Application Inference Profiles (§9) are the AWS-native attribution path but are broken in LiteLLM (issue #18258, open as of Jun 2026). Three viable alternatives cover every granularity requirement.
Mechanism 1 — IAM Principal Attribution (GA April 2026, Recommended)
AWS natively records the caller IAM identity on every bedrock-runtime call and maps principal tags to Cost Explorer. No API changes to LiteLLM calls required.
Option A — Static role tag per project (simplest):
aws iam create-role --role-name BedrockRole-Mantle \
--assume-role-policy-document file://trust.json
aws iam tag-role --role-name BedrockRole-Mantle \
--tags Key=Project,Value=Mantle Key=CostCenter,Value=CC-1042
LiteLLM config.yaml:
model_list:
- model_name: bedrock-claude-mantle
litellm_params:
model: bedrock/anthropic.claude-opus-4-5
aws_role_name: arn:aws:iam::123456789012:role/BedrockRole-Mantle
aws_session_name: litellm-mantle
aws_region_name: us-east-1
Trust policy on BedrockRole-Mantle must allow sts:AssumeRole and sts:TagSession from the LiteLLM execution role.
Option B — STS session tags on a shared role (dynamic, multi-tenant):
import boto3
from functools import lru_cache
@lru_cache(maxsize=64)
def get_project_credentials(project_name: str):
sts = boto3.client("sts")
return sts.assume_role(
RoleArn="arn:aws:iam::123456789012:role/BedrockSharedRole",
RoleSessionName=f"litellm-{project_name}",
Tags=[
{"Key": "Project", "Value": project_name},
{"Key": "CostCenter", "Value": PROJECT_COST_CENTERS[project_name]},
],
)["Credentials"]
Do not call AssumeRole per request — STS default quota is 500 calls/sec/account. Cache credentials for session lifetime (up to 12h).
Activation (one-time): AWS Billing console → Cost Allocation Tags → filter type “IAM principal” → activate Project and CostCenter. Allow 24h propagation. Tags appear in CUR 2.0 with prefix iamPrincipal/Project.
Granularity: per-usage-type per-day per-identity. Not per-request.
Limitation: Only bedrock-runtime APIs. Does not apply to Bedrock Agents or Knowledge Bases managed APIs.
Mechanism 2 — requestMetadata + S3/Athena (Per-Request Detail)
LiteLLM passes requestMetadata natively to the Bedrock Converse API since PR #14570 (Sep 22, 2025; use LiteLLM ≥ 1.53). This does not appear in Cost Explorer — it appears in Bedrock invocation logs only.
Enable Bedrock invocation logging:
aws bedrock put-model-invocation-logging-configuration \
--logging-config '{
"s3Config": {"bucketName": "my-bedrock-logs", "keyPrefix": "bedrock-logs/"},
"textDataDeliveryEnabled": true,
"imageDataDeliveryEnabled": false,
"embeddingDataDeliveryEnabled": false
}'
Pass metadata on every call:
response = litellm.completion(
model="bedrock/anthropic.claude-opus-4-5",
messages=[{"role": "user", "content": "..."}],
requestMetadata={
"project": "Mantle",
"virtual_key": "sk-mantle-prod",
"environment": "prod",
"feature": "contract-review",
}
)
Limits: max 16 key-value pairs, keys/values max 256 chars.
Athena cost query:
SELECT
requestMetadata.project AS project,
modelId,
SUM(input.inputTokenCount) AS input_tokens,
SUM(output.outputTokenCount) AS output_tokens,
ROUND(SUM(input.inputTokenCount) * 0.000015, 4) AS est_input_cost_usd,
ROUND(SUM(output.outputTokenCount) * 0.000075, 4) AS est_output_cost_usd
FROM bedrock_invocation_logs
WHERE requestMetadata.project = 'Mantle'
AND dt BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY 1, 2
ORDER BY est_input_cost_usd DESC;
Token-count-based cost is an estimate — multiply against current Bedrock pricing. For invoice-accurate totals, join to CUR at the model+usage-type grain (no per-request ID in CUR line items).
Mechanism 3 — LiteLLM SpendLogs as Internal Chargeback
LiteLLM’s LiteLLM_SpendLogs table is a fully viable substitute for Cost Explorer chargeback at per-request granularity. Assign one virtual key per project.
Create a virtual key for a project:
curl -X POST https://your-litellm-proxy/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-d '{
"team_id": "team-mantle",
"metadata": {"project": "Mantle"},
"tags": ["project:mantle", "env:prod"],
"key_alias": "mantle-prod"
}'
Per-project spend query:
SELECT
v.team_id,
DATE_TRUNC('day', s."startTime") AS day,
SUM(s.spend) AS cost_usd,
SUM(s.prompt_tokens) AS input_tokens,
SUM(s.completion_tokens) AS output_tokens
FROM "LiteLLM_SpendLogs" s
JOIN "LiteLLM_VerificationToken" v ON s.api_key = v.token
WHERE v.team_id = 'team-mantle'
AND s."startTime" >= '2026-06-01'
GROUP BY 1, 2
ORDER BY 2 DESC;
Or via API: GET /global/spend/report?group_by=team
Critical caveat: LiteLLM calculates spend using its own rate card, not the AWS invoice. Rates drift if you have reserved capacity, batch pricing, or negotiated rates. Use SpendLogs for quota enforcement and allocation; reconcile to CUR for invoice accuracy.
Scaling: At >1M rows, enable GCS/BigQuery callback in config.yaml to stream SpendLogs out of Postgres.
Decision Matrix
| Mechanism | In Cost Explorer | Per-request | LiteLLM config | Works today |
|---|---|---|---|---|
| IAM role tag (static) | Yes (iamPrincipal/) |
No (daily) | aws_role_name per model |
Yes (GA Apr 2026) |
| STS session tags | Yes (iamPrincipal/) |
No (daily) | AssumeRole hook + aws_role_name |
Yes (GA Apr 2026) |
| requestMetadata + Athena | No (logs only) | Yes | requestMetadata param |
Yes (LiteLLM ≥1.53) |
| Separate AWS account | Yes (native, zero config) | No (daily) | Separate credentials per model | Yes |
| LiteLLM SpendLogs | No (internal) | Yes | Virtual keys + team_id |
Yes |
| Application Inference Profiles | Yes | No (daily) | Model ARN | Broken (#18258) |
Recommended Architecture (Three Layers)
Run all three in parallel — they are independent and non-conflicting:
-
Billing-accurate daily totals in Cost Explorer: IAM session tags (Mechanism 1B). One
AssumeRolecall per project session, cached. ActivateProjectas cost allocation tag in Billing console. -
Per-prompt attribution for audit/debugging: Bedrock invocation logging +
requestMetadata(Mechanism 2). Ships to S3, queryable via Athena or CloudWatch Logs Insights. -
Near-real-time quota enforcement and internal chargeback: LiteLLM SpendLogs (Mechanism 3). One virtual key per project, query Postgres or export to warehouse.
Sources: AWS Bedrock IAM principal attribution (GA Apr 9, 2026), AWS Bedrock request-level metadata (GA May 2026), LiteLLM PR #14570, LiteLLM issue #18258.