Skip to main content

Production Readiness

You've deployed a LangGraph agent to Azure Container Apps. It works. Now you need it to work reliably under load, survive failures, stay secure, and cost something you can explain to finance.

This guide codifies our team's production standards for Container Apps. Every recommendation includes a concrete configuration, a rationale, and a tradeoff. Treat this as the authoritative reference when moving agents from "it runs" to "it runs in production."


What "Production-Ready" Means​

A production-ready Container Apps deployment satisfies five properties:

PropertyWhat It Looks Like
ResilientHealth probes catch failures before users do. Revisions enable instant rollback.
SecureNo hardcoded secrets. Managed identity for every Azure service. Key Vault for everything else.
ObservableStructured logs, distributed traces, and dashboards that answer "why is it slow?" in under 60 seconds.
ScalableAuto-scaling matches demand without over-provisioning. Costs are predictable.
RecoverableCross-region redundancy, tested failover, and documented runbooks.

Scaling Strategies​

Container Apps uses KEDA (Kubernetes Event-driven Autoscaling) for horizontal scaling. Getting scaling right is the single biggest lever for both performance and cost.

KEDA Auto-Scaling Configuration​

Every revision has a scale definition with limits and rules. Limits bound the replica count; rules determine when replicas are added or removed.

az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--image "aicaeng1234.azurecr.io/research-agent:1.0.0" \
--min-replicas 1 \
--max-replicas 10 \
--scale-rule-name "http-scale" \
--scale-rule-type http \
--scale-rule-http-concurrency 25 \
--cpu 2 \
--memory 4Gi

Key Parameters​

ParameterRecommended DefaultMinMaxNotes
minReplicas101000Set to 1 for always-on agents to avoid cold starts.
maxReplicas1011000Cap conservatively. LangGraph agents are expensive per replica.
concurrentRequests251β€”KEDA evaluates concurrent requests every 15 seconds.
Understanding concurrentRequests

KEDA calculates concurrent requests as (requests in the last 15 seconds) / 15. A value of 25 means a new replica is spawned when the rolling 15-second average exceeds 25 requests. For LangGraph agents that take 5-15 seconds per invocation, this means each replica handles roughly 2-5 concurrent requests before scaling out.

CPU and Memory Limits for LangGraph Agents​

LangGraph agents are memory-intensive due to LLM context windows, checkpointing, and tool execution. Right-sizing prevents OOM kills and wasted spend.

Agent TypeCPUMemoryRationale
Simple Q&A (single tool, small context)1 vCPU2GiLight LLM calls, minimal tool use.
Standard RAG (search + synthesis)2 vCPU4GiHybrid search, vector embeddings, multi-step reasoning.
Complex orchestration (multi-agent, long context)4 vCPU8GiMultiple LLM calls, large checkpoints, tool chains.
Batch processing (queue-driven)2 vCPU4GiProcessed sequentially; memory-bound, not CPU-bound.
OOM Kills in Container Apps

When a replica exceeds its memory limit, the container is killed with exit code 137. This is the most common cause of agent failures in production. Monitor for OOMKilled events in Log Analytics and increase memory before increasing replicas.

Concurrency Settings​

The concurrentRequests parameter controls how many in-flight requests a single replica handles before KEDA creates another. For LangGraph agents:

  • Default KEDA behavior: Every 15 seconds, KEDA computes concurrent_requests = requests_last_15s / 15.
  • Agent invocation time matters: If your agent takes 10 seconds to respond, a single replica can only handle ~1.5 requests in 15 seconds. Set concurrentRequests low (e.g., 5-10) to scale aggressively.
  • Fast agents: If your agent responds in < 2 seconds, you can set concurrentRequests higher (e.g., 50-100).

Tuning Formula​

concurrentRequests = (target_replicas * 15) / avg_agent_response_time

For a target of 3 replicas and an average response time of 8 seconds:

concurrentRequests = (3 * 15) / 8 β‰ˆ 6

Scale-to-Zero Considerations​

Container Apps can scale to zero replicas when minReplicas is 0. This saves money but introduces cold start latency.

ScenariominReplicasWhy
User-facing agent (low latency required)1Cold starts add 10-30 seconds. Users notice.
Internal tool (used intermittently)0Cost savings outweigh occasional cold starts.
Batch/nightly jobs0Only runs on schedule. Zero idle cost.
Always-on monitoring agent1Must be available for health checks.
Cold Start Impact

A cold start for a Python/LangGraph agent typically takes 10-30 seconds (dependency loading, model initialization, database connection pooling). If you set minReplicas to 0, the first request after scale-to-zero will experience this full latency.

Custom Scalers for Queue-Based Workloads​

For agents that process messages from Azure Service Bus, Azure Queue Storage, or Kafka, use custom KEDA scalers instead of HTTP scaling.

az containerapp create \
--name "batch-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--image "aicaeng1234.azurecr.io/batch-agent:1.0.0" \
--min-replicas 0 \
--max-replicas 20 \
--scale-rule-name "servicebus-scale" \
--scale-rule-type custom \
--scale-rule-metadata "type=azure-servicebus" \
--scale-rule-metadata "queueName=agent-tasks" \
--scale-rule-metadata "messageCount=5" \
--scale-rule-auth "identity=system"

This configuration scales the agent based on queue depth. When more than 5 messages are pending, KEDA adds replicas up to the maximum. When the queue drains, replicas scale back to zero.

Cost Implications​

StrategyMonthly Cost (est.)LatencyBest For
minReplicas: 0, maxReplicas: 5, HTTP scale$15-30High (cold starts)Intermittent internal tools
minReplicas: 1, maxReplicas: 5, HTTP scale$40-80LowUser-facing agents
minReplicas: 2, maxReplicas: 10, HTTP scale$80-200Very lowHigh-availability agents
Queue-based, minReplicas: 0$10-25N/A (async)Batch processing
Cost Control

Set maxReplicas conservatively and monitor actual peak usage for one week before increasing. LangGraph agents are expensive β€” a single replica at 4 vCPU / 8Gi costs roughly $100-150/month when running 24/7.


Secrets Management​

Every credential, API key, and connection string must be managed through proper secrets infrastructure. Never hardcode secrets in Docker images, environment variables, or source code.

Using Azure Key Vault for Secrets​

Azure Key Vault is our team's single source of truth for all secrets. Container Apps integrates natively with Key Vault through managed identity.

# 1. Create the secret in Key Vault
az keyvault secret set \
--vault-name "kv-ai-engineering" \
--name "langfuse-secret-key" \
--value "sk-lf-xxxxxxxxxxxx"

# 2. Reference the Key Vault secret in Container Apps
# (use the versionless URI for automatic rotation)
az containerapp secret set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--secrets "langfuse-key=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-secret-key,identityref:system"

# 3. Expose as environment variable via secret reference
az containerapp appsettings set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--settings "LANGFUSE_SECRET_KEY=secretref:langfuse-key"

Managed Identity Authentication​

Managed identity is our team's mandatory standard for authenticating to Azure services. It eliminates credential management entirely.

# Enable system-assigned managed identity
az containerapp identity assign \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--system-assigned

# Get the principal ID for role assignments
PRINCIPAL_ID=$(az containerapp identity show \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--query principalId \
--output tsv)

# Grant Key Vault access
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Key Vault Secrets User" \
--scope "/subscriptions/SUB_ID/resourceGroups/rg-ai-engineering/providers/Microsoft.KeyVault/vaults/kv-ai-engineering"

Application Code Pattern​

# src/config.py
import os
from azure.identity import DefaultAzureCredential
from langchain_openai import AzureChatOpenAI

class Settings:
"""Application settings β€” managed identity for all Azure services."""

credential = DefaultAzureCredential()

llm = AzureChatOpenAI(
model="gpt-4o",
azure_deployment="gpt-4o",
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version="2024-10-01-preview",
azure_ad_token_provider=lambda: credential.get_token(
"https://cognitiveservices.azure.com/.default"
).token,
)

# Key Vault access for non-Azure secrets
key_vault_credential = DefaultAzureCredential()
Managed Identity Token Caching

The managed identity backend caches tokens for approximately 24 hours per resource URI. If you update RBAC permissions on a target resource, the old cached token may still be used until it expires. Plan permission changes accordingly.

Rotating Secrets Without Downtime​

When a Key Vault secret URI omits the version suffix, Container Apps automatically picks up the latest version within 30 minutes and restarts affected revisions.

URI FormatBehavior
https://kv.vault.azure.net/secrets/my-secretVersionless β€” auto-rotates within 30 min.
https://kv.vault.azure.net/secrets/my-secret/ec96f02...Versioned β€” pins to specific version. No auto-rotation.

Our standard: Always use versionless URIs in production. This enables zero-downtime secret rotation.

# Rotate a secret in Key Vault β€” Container Apps picks it up automatically
az keyvault secret set \
--vault-name "kv-ai-engineering" \
--name "langfuse-secret-key" \
--value "sk-lf-NEWVALUE"

Environment-Specific Secrets​

Use separate Key Vaults per environment and parameterize the secret URIs in your IaC:

param environment string = 'production'

var keyVaultName = {
development: 'kv-ai-eng-dev'
staging: 'kv-ai-eng-staging'
production: 'kv-ai-eng-prod'
}[environment]

var keyVaultSecretUri = 'https://${keyVaultName}.vault.azure.net/secrets/langfuse-secret-key'

The Golden Rule​

Never hardcode API keys. If a secret exists, it belongs in Key Vault. If it's in Key Vault, it's accessed via managed identity. If it's accessed via managed identity, it's never in your code, your Docker image, or your CI/CD logs.


Health Probes​

Health probes are how Container Apps determines whether your agent is alive, ready to receive traffic, or stuck. Misconfigured probes are the #1 cause of unnecessary container restarts and missed failures.

Probe Types​

ProbePurposeFailure Action
StartupChecks if the app started successfully. Runs only during initial boot.Restarts the container after failureThreshold attempts.
LivenessChecks if the app is still running and responsive.Restarts the container.
ReadinessChecks if the replica should receive traffic.Removes the replica from the load balancer pool.

Default Configuration​

When ingress is enabled, Container Apps adds default TCP probes automatically:

ProbeProtocolTimeoutPeriodInitial DelayFailure Threshold
StartupTCP3s1s1s240
LivenessTCPβ€”β€”β€”β€”
ReadinessTCP5s5s3s48
TCP vs. HTTP Probes

Default probes use TCP (connection check). For LangGraph agents, always override with HTTP probes that validate application-level health β€” database connectivity, LLM availability, and agent initialization.

Configuring Probe Endpoints in FastAPI​

# src/main.py
import os
import time
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from psycopg_pool import ConnectionPool

startup_time = time.time()
db_pool: ConnectionPool | None = None


@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifecycle β€” initialize resources at startup."""
global db_pool
db_pool = ConnectionPool(
conninfo=os.environ["LANGGRAPH_DB_URI"],
min_size=3,
max_size=10,
timeout=5,
open=True,
)
yield
if db_pool:
await db_pool.close()


app = FastAPI(
title="Research Agent API",
version=os.environ.get("AGENT_VERSION", "1.0.0"),
lifespan=lifespan,
)


@app.get("/health")
async def health_check():
"""
Liveness probe endpoint.

Returns 200 if the application core is functional.
Returns 503 if critical dependencies are down.
"""
try:
# Check database connectivity
if db_pool:
async with db_pool.connection() as conn:
await conn.execute("SELECT 1")
return {"status": "healthy", "uptime": int(time.time() - startup_time)}
except Exception:
raise HTTPException(status_code=503, detail="Database unreachable")


@app.get("/ready")
async def readiness_check():
"""
Readiness probe endpoint.

Returns 200 only when the agent is fully initialized and ready
to process requests β€” including LLM connectivity.
"""
try:
# Check database
if db_pool:
async with db_pool.connection() as conn:
await conn.execute("SELECT 1")

# Check that the agent is initialized
from src.agent import agent
if agent is None:
raise HTTPException(status_code=503, detail="Agent not initialized")

return {"status": "ready"}
except Exception as exc:
raise HTTPException(status_code=503, detail=str(exc))


@app.get("/startup")
async def startup_check():
"""
Startup probe endpoint.

Lightweight check that returns 200 once the FastAPI server
has bound to its port. Used to prevent liveness probe from
killing the container during slow initialization.
"""
return {"status": "started"}

Probe Timing Configuration​

resource containerApp 'Microsoft.App/containerApps@2025-02-02-preview' = {
name: 'research-agent'
properties: {
template: {
containers: [
{
name: 'agent'
image: 'aicaeng1234.azurecr.io/research-agent:1.0.0'
probes: [
{
type: 'Startup'
httpGet: {
path: '/startup'
port: 8000
}
initialDelaySeconds: 3
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
successThreshold: 1
}
{
type: 'Liveness'
httpGet: {
path: '/health'
port: 8000
}
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
}
{
type: 'Readiness'
httpGet: {
path: '/ready'
port: 8000
}
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
}
]
}
]
}
}
}
ParameterStartup ProbeLiveness ProbeReadiness Probe
initialDelaySeconds31510
periodSeconds5105
timeoutSeconds355
failureThreshold3033
successThreshold111
Slow-Starting Agents

LangGraph agents can take 15-30 seconds to initialize (loading models, establishing database pools, warming caches). The startup probe's high failureThreshold (30) with periodSeconds of 5 gives the container up to 150 seconds before a restart is triggered. This prevents the liveness probe from killing the container during normal initialization.

Probe Failure Troubleshooting​

When probes fail, Container Apps marks the revision as unhealthy. Common causes:

  1. Liveness probe fails repeatedly β†’ The app is stuck or crashing. Check logs for exceptions.
  2. Readiness probe fails but liveness passes β†’ The app is running but dependencies are down. Check database and LLM connectivity.
  3. Startup probe times out β†’ The container image is broken or the app crashes during initialization.
// Log Analytics query: find probe failures
ContainerAppConsoleLogs_CL
| where Log_s contains "probe" or Log_s contains "unhealthy"
| project TimeGenerated, ContainerName_s, Log_s
| order by TimeGenerated desc
| limit 100

Revision Management​

Revisions are the deployment unit in Container Apps. Every revision is an immutable snapshot of your app's configuration. Understanding revisions is essential for safe deployments.

What Are Revisions​

A revision is created automatically when you deploy your app for the first time. New revisions are created when you make revision-scope changes:

Change TypeCreates New Revision?Examples
Revision-scopeYesContainer image, environment variables, scale rules, resource limits, probes
Application-scopeNoSecret values, ingress configuration, traffic splitting rules, labels

Rollout Strategies​

Single Revision Mode (Default)​

# Single revision mode β€” automatic zero-downtime rollout
az containerapp revision set-mode \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--mode single

In single revision mode, Container Apps:

  1. Provisions the new revision alongside the old one.
  2. Waits for all replicas to pass startup and readiness probes.
  3. Shifts 100% of traffic to the new revision.
  4. Deprovisions the old revision.

Use this for: Standard deployments where you want automatic, safe rollouts.

Multiple Revision Mode​

# Multiple revision mode β€” manual traffic control
az containerapp revision set-mode \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--mode multiple

In multiple revision mode, you manually:

  1. Activate the new revision.
  2. Configure traffic splitting.
  3. Deactivate old revisions when ready.

Use this for: Canary deployments, A/B testing, and blue-green deployments.

Rollback Procedures​

Rollback is fast because revisions are immutable. To roll back:

# List all revisions to find the stable one
az containerapp revision list \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
-o table

# Copy the stable revision to create a new one with the same config
az containerapp revision copy \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--source-revision "research-agent-abc123"
Rollback Checklist

When rolling back, also check:

  • Were there application-scope changes (new secrets, ingress changes)? These persist across revisions.
  • Did the rollback revision use a different Key Vault secret version? Versionless URIs auto-rotate.
  • Are there pending database migrations that the old revision doesn't support?

Revision Suffixes for Custom DNS​

Customize revision names for easier identification:

az containerapp update \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--revision-suffix "v1-gpt4o-rag"

The revision name becomes research-agent-v1-gpt4o-rag. Use descriptive suffixes that encode the version and key changes.

Traffic Splitting for Canary Deployments​

In multiple revision mode, split traffic between revisions:

# Deploy new revision
az containerapp update \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--image "aicaeng1234.azurecr.io/research-agent:2.0.0"

# Split traffic: 90% old, 10% new
az containerapp traffic set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--revision "research-agent-old-rev" --weight 90 \
--revision "research-agent-new-rev" --weight 10

Canary rollout process:

  1. Deploy new revision (10% traffic).
  2. Monitor error rates and latency for 30 minutes.
  3. If stable, increase to 50/50.
  4. Monitor another 30 minutes.
  5. If stable, shift to 100% new revision.
  6. Deactivate old revision.

Observability​

Without observability, you're flying blind. Container Apps integrates with Azure Monitor and Application Insights, but you need to configure it properly.

Application Insights Integration​

Application Insights is automatically provisioned with Container Apps environments. Verify it's capturing data:

# Check the connected Log Analytics workspace
az containerapp env show \
--name "cae-ai-agents" \
--resource-group "rg-ai-engineering" \
--query "properties.appInsightsDataCollectionEndpoint"

Distributed Tracing with Langfuse​

Langfuse provides end-to-end tracing for agent runs. Configure it at the application level:

# Store Langfuse credentials as Key Vault-backed secrets
az containerapp secret set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--secrets \
"langfuse-pk=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-public-key,identityref:system" \
"langfuse-sk=keyvaultref:https://kv-ai-engineering.vault.azure.net/secrets/langfuse-secret-key,identityref:system"

az containerapp appsettings set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--settings \
"LANGFUSE_PUBLIC_KEY=secretref:langfuse-pk" \
"LANGFUSE_SECRET_KEY=secretref:langfuse-sk" \
"LANGFUSE_HOST=https://cloud.langfuse.com"

Custom Metrics and Dashboards​

Create an Application Insights dashboard with these key metrics:

MetricSourceAlert Threshold
HTTP 5xx rateApplication Insights> 1% over 5 min
Agent response latency (p95)Custom metric> 30s
Replica countContainer Apps metrics> maxReplicas for 10 min
OOM kill countKubeEvents> 0 in 1 hour
Probe failure countContainerAppReplicas> 3 in 5 min
LLM token costCustom metric (Langfuse)Budget-dependent

Custom Metric Emission​

# src/metrics.py
import time
from opencensus.ext.azure import log_metrics

metrics_client = log_metrics.get_metrics_client()


def track_agent_invocation(duration_ms: int, step_count: int, status: str) -> None:
"""Emit custom metrics for agent invocations."""
metrics_client.track_metric(name="agent.invocation.duration_ms", value=duration_ms)
metrics_client.track_metric(name="agent.invocation.step_count", value=step_count)
metrics_client.track_metric(name="agent.invocation.status", value=1, tags={"status": status})

Alert Rules for Common Failure Modes​

// Alert: High error rate
app()
| where ipv4_is_private(clientIp) == false
| where result_code >= 500
| summarize errorCount = count() by bin(timestamp, 5m)
| where errorCount > 10

// Alert: OOM kills
ContainerAppInstances
| where runningState has "OOMKilled" or runningState has "CrashLoopBackOff"
| summarize count() by ContainerAppName, bin(TimeGenerated, 1h)

// Alert: Replica count at maximum
ContainerAppReplicas
| summarize maxReplicas = max(replicasCount) by ContainerAppName, bin(TimeGenerated, 10m)
| where maxReplicas >= 10 // matches maxReplicas config

Log Retention and Cost Management​

Application Insights data retention defaults to 30 days. For cost management:

SettingRecommendation
Log retention30 days for development, 90 days for production.
SamplingEnable adaptive sampling at 50% for non-production.
Custom LogsFilter out DEBUG-level logs in production.
Dependency trackingEnable only for critical external calls (LLM, database).
Cost Control for Application Insights

Application Insights costs scale with data volume. A typical LangGraph agent emits 50-200 MB/day of logs. At default pricing, this is roughly $5-15/month per agent. Use data caps and sampling to control costs.


Networking​

Secure networking is non-negotiable for production agents that handle sensitive data and connect to multiple Azure services.

VNet Integration for Private Access​

VNet integration isolates your Container Apps environment from the public internet and enables private connectivity to Azure services.

# Create VNet with dedicated subnets
az network vnet create \
--name "vnet-ai-agents" \
--resource-group "rg-ai-engineering" \
--location "swedencentral" \
--address-prefixes "10.0.0.0/16" \
--subnet-name "cae-subnet" \
--subnet-prefixes "10.0.1.0/24"

# Get subnet ID
SUBNET_ID=$(az network vnet subnet show \
--vnet-name "vnet-ai-agents" \
--name "cae-subnet" \
--resource-group "rg-ai-engineering" \
--query id \
--output tsv)

# Create Container Apps environment with VNet integration
az containerapp env create \
--name "cae-ai-agents" \
--resource-group "rg-ai-engineering" \
--location "swedencentral" \
--infrastructure-subnet-id "$SUBNET_ID"

Custom Domains and TLS Certificates​

Production agents should use custom domains with TLS:

# Add a custom domain
az containerapp domain bind \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--domain-name "agent.yourcompany.com" \
--manual \
--cert-name "agent-tls-cert" \
--cert-file "/path/to/cert.pfx" \
--cert-key "your-certificate-password"
DNS Verification

When binding a custom domain, Container Apps provides a CNAME record you must add to your DNS zone. The domain won't work until DNS propagation completes (typically 5-15 minutes).

Ingress Configuration​

SettingInternalExternal
Reachable from internetNoYes
Reachable from same environmentYesYes
Custom domain supportLimitedFull
TLS terminationNoYes
# Internal ingress β€” only accessible within the VNet
az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--ingress internal \
--target-port 8000 \
--transport auto

# External ingress β€” publicly accessible
az containerapp create \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--environment "cae-ai-agents" \
--ingress external \
--target-port 8000 \
--transport auto

Our standard: Use internal ingress for all agent apps. Place an API Management instance or Azure Front Door in front for public-facing endpoints.

Rate Limiting​

Protect your agents from abuse and cost overruns:

# Configure IP restrictions on the Container App
az containerapp restriction set \
--name "research-agent" \
--resource-group "rg-ai-engineering" \
--action Allow \
--ip-address "10.0.0.0/8" \
--rule-name "internal-network"

For application-level rate limiting, use FastAPI middleware:

# src/middleware.py
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi.responses import JSONResponse

limiter = Limiter(key_func=get_remote_address)


@limiter.errorhandler
def handle_rate_limit(error: RateLimitExceeded):
return JSONResponse(
status_code=429,
content={"detail": "Too many requests. Try again later."},
)


# Apply to the app
app.state.limiter = limiter


@app.post("/chat")
@limiter.limit("10/minute")
async def chat(request: ChatRequest):
# ... agent invocation
pass

Disaster Recovery​

Production agents must survive regional outages. This section covers our team's disaster recovery standards.

Cross-Region Deployment Strategies​

Deploy agent replicas to at least two Azure regions. Use Azure Traffic Manager or Front Door for failover routing.

StrategyRTORPOComplexityCost
Cold standby (secondary region, no active replicas)15-30 minLast successful backupLow~20% of primary
Warm standby (secondary region, minReplicas: 1)1-5 minNear-zeroMedium~50% of primary
Active-active (both regions serving traffic)< 1 minNear-zeroHigh~100% of primary

Our standard: Warm standby for user-facing agents. Cold standby for internal tools.

Warm Standby Configuration​

# Primary region
PRIMARY_RG="rg-ai-engineering-se"
PRIMARY_REGION="swedencentral"

# Secondary region
SECONDARY_RG="rg-ai-engineering-nee"
SECONDARY_REGION="northeurope"

# Deploy to secondary with minReplicas: 1 (warm standby)
az containerapp create \
--name "research-agent" \
--resource-group "$SECONDARY_RG" \
--environment "cae-ai-agents-secondary" \
--image "aicaeng1234.azurecr.io/research-agent:1.0.0" \
--min-replicas 1 \
--max-replicas 5 \
--target-port 8000 \
--ingress external

Backup and Restore Procedures​

Container Apps itself doesn't provide built-in backup, but your data stores (PostgreSQL, Azure AI Search, Key Vault) do.

ResourceBackup MethodFrequencyRetention
PostgreSQLAzure automated backupContinuous (point-in-time restore)7-35 days (depends on tier)
Azure AI SearchIndex snapshots via Azure BackupDaily30 days
Key VaultBuilt-in soft delete + purge protectionContinuous90 days
Container App configBicep/ARM templates in GitOn every changeInfinite (Git history)

PostgreSQL Point-in-Time Restore​

# Restore PostgreSQL to a specific point in time
az postgres flexible-server restore \
--name "research-agent-db-restore" \
--resource-group "rg-ai-engineering" \
--source-server "research-agent-db" \
--restore-point-time "2025-01-15T10:30:00Z"

Testing Failover Regularly​

Disaster recovery is only as good as your last test. Schedule quarterly failover drills:

  1. Simulate primary region failure β€” deactivate the primary Container App revision.
  2. Verify secondary region handles traffic β€” check health endpoints and agent responses.
  3. Measure actual RTO β€” time from failure detection to full recovery.
  4. Document findings β€” update runbooks with actual timings and any issues found.
Failover Testing Checklist

Before each failover drill:

  • Notify the team (scheduled maintenance window).
  • Verify secondary region has current Key Vault secrets.
  • Confirm database replication lag is within acceptable bounds.
  • Have a rollback plan ready (re-activate primary).
  • Assign an observer to time each step.

Summary Checklist​

Use this checklist when promoting an agent to production:

  • Scaling: minReplicas: 1, maxReplicas set, concurrentRequests tuned to agent response time.
  • Resources: CPU and memory right-sized for agent complexity. OOM monitoring in place.
  • Secrets: All credentials in Key Vault. Managed identity for Azure services. No hardcoded secrets.
  • Probes: HTTP startup, liveness, and readiness probes configured with appropriate timing.
  • Revisions: Revision mode set. Rollback procedure documented.
  • Observability: Application Insights active. Langfuse tracing enabled. Alert rules configured.
  • Networking: VNet integration enabled. Internal ingress with API Management/Front Door. IP restrictions applied.
  • Disaster Recovery: Secondary region deployed. Backup procedures verified. Failover runbook written.

Next Steps​