Skip to main content

How to Deploy an AI Chatbot on Azure Using Azure OpenAI and App Service

Step-by-Step GuideAzure OpenAIApp ServiceProductionPython

How to Deploy an AI Chatbot on Azure
Using Azure OpenAI and App Service

From zero to a production-grade AI chatbot: provision Azure OpenAI, write a streaming Flask API backend, deploy it on Azure App Service with Managed Identity, wire in conversation history and content safety, and instrument it with Application Insights — all with complete code and Terraform IaC. No API keys in environment variables. No hardcoded secrets. No half-finished PoC patterns.

7 phases
This guide covers the full deployment lifecycle: architecture design → resource provisioning → backend code → App Service deployment → streaming → security → monitoring
Zero keys
The chatbot authenticates to Azure OpenAI using Managed Identity and DefaultAzureCredential — no API keys stored in environment variables, Key Vault, or code
SSE
Server-Sent Events stream GPT tokens to the browser as they generate — the same token-by-token typing effect users expect from production AI products
~45 min
Estimated time from first Azure CLI command to a live, streaming, authenticated chatbot running on App Service — following this guide step by step

Architecture Overview: All Components and How They Connect

The architecture for a production Azure OpenAI chatbot has five distinct layers: the user's browser, the App Service hosting the chatbot backend, the Azure OpenAI service processing the completions, the supporting services (Key Vault, Azure Cache for Redis for session storage, Application Insights for observability), and the identity layer tying them together without credentials. Understanding how these layers connect before writing a single line of code prevents the most common production failures — API keys leaked into logs, conversation history stored in server memory (destroyed on every App Service restart), and no visibility into model response times or error rates.

Figure 1 — Production AI chatbot architecture on Azure: all components, data flows, and identity boundaries
BrowserUser sendschat messageHTTPSSSE streamAzure App ServiceFlask API backend• /api/chat (streaming)• /api/health• System prompt injection• History retrievalSystem-Assigned MIDefaultAzureCredentialBearerTokenAzure OpenAIDeployed Model:gpt-4.1-miniChat Completions APIstream=True → SSEContent filteringTPM / RPM quotasPrivate endpointAzure Cache for RedisConversation historySession stateKey VaultRedis conn stringConfig secretsApp InsightsRequest tracesToken usage metricsMicrosoft Entra IDIssues short-livedBearer tokens forApp Service MINo API key storedMI tokenrequestCore chat path---Identity / token flowSupporting services (accessed via MI)
The App Service's System-Assigned Managed Identity requests a short-lived Bearer token from Entra ID and presents it to Azure OpenAI. No API key is stored anywhere. Supporting services (Redis for history, Key Vault for config, App Insights for telemetry) are also accessed via the same Managed Identity.

Choosing the Right Azure OpenAI Model for Your Chatbot

Model selection determines the balance between response quality, latency, and cost. For most chatbot deployments, the correct choice is not the most powerful model available — it is the model that delivers acceptable quality at the latency and cost target your use case requires.

ModelContext WindowBest ForApproximate CostLatency
gpt-4.1-mini1M tokensHigh-volume chatbots, internal assistants, customer support automation. Best cost-to-quality ratio for conversational workloads in 2026.$0.40/$1.60 per 1M tokens (in/out)Fast — recommended default
gpt-4.11M tokensComplex reasoning, multi-step analysis, document-heavy chatbots. Use when gpt-4.1-mini quality is insufficient.$2.00/$8.00 per 1M tokensModerate
gpt-5-nano128K tokensUltra-high-volume, cost-sensitive applications where speed matters more than depth. Simple FAQ bots, intent classification.$0.05/$0.40 per 1M tokensVery fast
gpt-5256K tokensMission-critical chatbots requiring the highest reasoning quality — legal assistants, medical triage, complex financial guidance.$1.25/$10.00 per 1M tokensModerate to slow
Recommendation for this guide

This guide uses gpt-4.1-mini as the deployment model. It delivers production-quality conversational responses, supports the full 1M-token context window for long conversation histories, streams tokens efficiently, and costs a fraction of gpt-4.1. Swap the deployment name to upgrade the model at any time — no code changes required.

Deployment Phases
Phase 1Provision Azure OpenAI and Deploy a Model
Start Here~10 min
1

Create a resource group

All chatbot resources will live in one resource group for clean lifecycle management. az group create --name rg-chatbot-prod --location eastus. Choose a region where your target model is available — check the Azure OpenAI model availability page for your subscription.

2

Create the Azure OpenAI resource

az cognitiveservices account create --name oai-chatbot-prod --resource-group rg-chatbot-prod --kind OpenAI --sku S0 --location eastus --custom-domain oai-chatbot-prod. The --custom-domain flag creates a subdomain at oai-chatbot-prod.openai.azure.com — required for Managed Identity authentication (API key auth uses a different endpoint format).

3

Deploy the gpt-4.1-mini model

az cognitiveservices account deployment create --name oai-chatbot-prod --resource-group rg-chatbot-prod --deployment-name gpt-4.1-mini --model-name gpt-4.1-mini --model-version "2025-04-14" --model-format OpenAI --sku-capacity 100 --sku-name "Standard". The --sku-capacity 100 allocates 100,000 TPM. Note the deployment name — it is the value you pass to the API as the model parameter.

4

Record the endpoint URL

az cognitiveservices account show --name oai-chatbot-prod --resource-group rg-chatbot-prod --query properties.endpoint -o tsv. The output will be https://oai-chatbot-prod.openai.azure.com/. This is the only value your application needs — no API key required when using Managed Identity.

Figure 2 — Streaming chat request lifecycle: from user keystroke to streamed token in browser
Each chat message triggers this sequence — streaming tokens arrive as they generate① UserTypes messagePOST /api/chatwith session_id② HistoryFetch prior turnsfrom Redis cacheby session_id③ Build ContextSystem prompt +history + new msg→ messages array④ Azure OpenAIstream=TrueAuth: Bearer token(from MI — no key)Content filter runs⑤ SSE StreamToken chunksdata: {token}\n\n→ browser appends⑥ On completion: save full response + new user message to Redis (with TTL)gpt-4.1-mini streams ~60–80 tokens/second — users see the response "typing" in real time, just like ChatGPT
The complete lifecycle of a single chat turn. Steps ①–③ take under 5ms. Step ④ is the Azure OpenAI network + generation time. Step ⑤ streams tokens to the browser as they arrive. Step ⑥ saves the completed exchange to Redis for future turns.
Phase 2Build the Flask Chatbot Backend with Streaming
Core CodePython / Flask

The backend has three endpoints: POST /api/chat (the streaming chat endpoint), DELETE /api/chat/<session_id> (clear conversation history), and GET /api/health (App Service health probe). The streaming endpoint uses Server-Sent Events (SSE) — the Flask response is a generator that yields token chunks as they arrive from Azure OpenAI.

requirements.txtflask==3.1.0
openai==1.84.0
azure-identity==1.23.0
azure-keyvault-secrets==4.10.0
redis==5.2.0
opencensus-ext-azure==1.1.14
gunicorn==23.0.0
app.py — Complete Flask chatbot backend with SSE streaming# app.py — Azure OpenAI chatbot backend
import os, json, time, uuid, logging
from flask import Flask, request, Response, jsonify, stream_with_context
from openai import AzureOpenAI
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from azure.keyvault.secrets import SecretClient
import redis

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ── CONFIGURATION ────────────────────────────────────────
AZURE_OPENAI_ENDPOINT = os.environ["AZURE_OPENAI_ENDPOINT"]
OPENAI_DEPLOYMENT = os.environ.get("OPENAI_DEPLOYMENT", "gpt-4.1-mini")
KV_URL = os.environ.get("KEY_VAULT_URL") # e.g. https://kv-chatbot.vault.azure.net

SYSTEM_PROMPT = """You are a helpful, professional AI assistant.
You answer questions clearly and concisely.
If you don't know something, say so honestly.
Never make up facts or cite sources you're not certain of."""

# ── MANAGED IDENTITY AUTH TO AZURE OPENAI ───────────────
# No API key. DefaultAzureCredential uses the App Service's
# System-Assigned Managed Identity automatically in production.
credential = DefaultAzureCredential()
token_provider = get_bearer_token_provider(
    credential,
    "https://cognitiveservices.azure.com/.default"
)
openai_client = AzureOpenAI(
    azure_endpoint=AZURE_OPENAI_ENDPOINT,
    azure_ad_token_provider=token_provider, # MI token, not API key
    api_version="2024-12-01-preview"
)

# ── REDIS FOR CONVERSATION HISTORY ──────────────────────
def get_redis_client():
    if KV_URL:
        kv_client = SecretClient(vault_url=KV_URL, credential=credential)
        redis_conn = kv_client.get_secret("redis-connection-string").value
    else:
        redis_conn = os.environ.get("REDIS_CONNECTION", "redis://localhost:6379")
    return redis.from_url(redis_conn, decode_responses=True)

redis_client = get_redis_client()
HISTORY_TTL = 3600 # Expire sessions after 1 hour of inactivity
MAX_HISTORY_TURNS = 20 # Keep last 20 turns (40 messages)

# ── CONVERSATION HISTORY HELPERS ─────────────────────────
def get_history(session_id: str) -> list:
    try:
        raw = redis_client.get(f"chat:history:{session_id}")
        return json.loads(raw) if raw else []
    except Exception as e:
        logger.warning(f"Redis read failed: {e}")
        return [] # Degrade gracefully — stateless conversation

def save_history(session_id: str, history: list):
    try:
        # Trim to last MAX_HISTORY_TURNS exchanges
        trimmed = history[-(MAX_HISTORY_TURNS * 2):]
        redis_client.setex(f"chat:history:{session_id}",
            HISTORY_TTL, json.dumps(trimmed))
    except Exception as e:
        logger.warning(f"Redis write failed: {e}")

# ── STREAMING CHAT ENDPOINT ──────────────────────────────
@app.route("/api/chat", methods=["POST"])
def chat():
    data = request.get_json()
    user_message = data.get("message", "").strip()
    session_id = data.get("session_id") or str(uuid.uuid4())

    if not user_message:
        return jsonify({"error": "message is required"}), 400

    # Build messages array: system + history + new user message
    history = get_history(session_id)
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT}
    ] + history + [{"role": "user", "content": user_message}]

    def generate_stream():
        full_response = ""
        try:
            stream = openai_client.chat.completions.create(
                model=OPENAI_DEPLOYMENT,
                messages=messages,
                stream=True, # Token-by-token streaming
                max_tokens=1024,
                temperature=0.7,
            )
            # Yield session_id first so client can track the session
            yield f"data: {json.dumps({'session_id': session_id})}\n\n"

            for chunk in stream:
                delta = chunk.choices[0].delta if chunk.choices else None
                if delta and delta.content:
                    full_response += delta.content
                    yield f"data: {json.dumps({'token': delta.content})}\n\n"

            # Stream complete — save exchange to Redis
            history.append({"role": "user", "content": user_message})
            history.append({"role": "assistant", "content": full_response})
            save_history(session_id, history)
            yield "data: [DONE]\n\n"

        except Exception as e:
            logger.error(f"OpenAI error: {e}")
            yield f"data: {json.dumps({'error': str(e)})}\n\n"

    return Response(
        stream_with_context(generate_stream()),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no", # Disable nginx buffering
        }
    )

@app.route("/api/chat/<session_id>", methods=["DELETE"])
def clear_history(session_id):
    redis_client.delete(f"chat:history:{session_id}")
    return jsonify({"cleared": session_id})

@app.route("/api/health")
def health():
    return jsonify({"status": "ok", "model": OPENAI_DEPLOYMENT})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=False)
Phase 3Conversation History Management
State LayerRedis

Storing conversation history in the App Service process memory is the single most common production mistake in chatbot deployments. App Service instances restart, scale out to multiple instances, and receive different requests on different instances — process memory is not shared and not durable. Any history stored in memory is lost on the next restart or load-balanced to a different instance.

Azure Cache for Redis is the correct solution: it is a distributed in-memory store accessible from any App Service instance, with configurable TTL for automatic session expiry. The history stored in Redis is a JSON-serialised array of {role, content} objects — exactly the format the Azure OpenAI Chat Completions API requires.

1

Create Azure Cache for Redis

az redis create --name redis-chatbot-prod --resource-group rg-chatbot-prod --location eastus --sku Basic --vm-size c1. Basic C1 (1GB) is sufficient for most chatbot deployments. Use Standard or Premium for production SLA guarantees and geo-replication.

2

Store the Redis connection string in Key Vault

Get the connection string: az redis list-keys --name redis-chatbot-prod --resource-group rg-chatbot-prod --query primaryKey -o tsv. The full connection string format: redis-chatbot-prod.redis.cache.windows.net:6380,password=KEY,ssl=True. Store it: az keyvault secret set --vault-name kv-chatbot-prod --name redis-connection-string --value "YOUR-CONN-STRING". The App Service retrieves it at startup via Managed Identity — no connection string in environment variables.

3

Set appropriate history TTL and turn limits

Configure two limits: HISTORY_TTL (seconds before an idle session expires — 3600 for 1 hour, 86400 for 24 hours) and MAX_HISTORY_TURNS (number of conversation turns to keep — 20 turns = 40 messages). The turn limit prevents history from growing unbounded and consuming your context window budget. With gpt-4.1-mini's 1M context window, you can afford longer histories — but longer histories increase per-request token costs.

Phase 4Deploy to Azure App Service
Deployment~15 min
1

Create App Service Plan and Web App

az appservice plan create --name asp-chatbot-prod --resource-group rg-chatbot-prod --is-linux --sku P1v3. Then: az webapp create --name app-chatbot-prod --resource-group rg-chatbot-prod --plan asp-chatbot-prod --runtime "PYTHON:3.12". Use P1v3 or higher for production — the B1 and B2 Basic tiers do not include autoscaling and have limited memory for Python apps with multiple dependencies.

2

Configure the startup command

az webapp config set --name app-chatbot-prod --resource-group rg-chatbot-prod --startup-file "gunicorn --bind=0.0.0.0 --timeout 600 --worker-class gevent --workers 4 app:app". The --timeout 600 is critical — streaming responses take longer than App Service's default 230-second timeout. Use gevent workers to handle concurrent SSE connections without blocking. Install gevent: add gevent==24.2.1 to requirements.txt.

3

Set application settings (environment variables)

az webapp config appsettings set --name app-chatbot-prod --resource-group rg-chatbot-prod --settings AZURE_OPENAI_ENDPOINT="https://oai-chatbot-prod.openai.azure.com/" OPENAI_DEPLOYMENT="gpt-4.1-mini" KEY_VAULT_URL="https://kv-chatbot-prod.vault.azure.net" SCM_DO_BUILD_DURING_DEPLOYMENT=true. Do not set an API key — authentication uses Managed Identity.

4

Deploy the application code

From the project root: az webapp deploy --name app-chatbot-prod --resource-group rg-chatbot-prod --src-path . --type zip. The zip deploy uploads your code, App Service installs dependencies from requirements.txt during build, and starts the gunicorn server. Monitor deployment: az webapp log tail --name app-chatbot-prod --resource-group rg-chatbot-prod.

5

Verify the deployment

curl https://app-chatbot-prod.azurewebsites.net/api/health. Expected response: {"status": "ok", "model": "gpt-4.1-mini"}. If the health endpoint returns a 500, check the application logs for startup errors — the most common cause is a missing environment variable or a Managed Identity RBAC assignment not yet propagated.

JavaScript — Frontend SSE client to stream chatbot responses// Frontend — send message and stream response token-by-token
async function sendMessage(message, sessionId = null) {
  const response = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, session_id: sessionId })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let currentSessionId = sessionId;

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // Parse SSE events from buffer
    const lines = buffer.split("\n");
    buffer = lines.pop(); // Keep incomplete line in buffer

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const payload = line.slice(6).trim();
      if (payload === "[DONE]") return currentSessionId;

      try {
        const data = JSON.parse(payload);
        if (data.session_id) currentSessionId = data.session_id;
        if (data.token) appendTokenToUI(data.token); // Update DOM
        if (data.error) showError(data.error);
      } catch (e) { /* ignore malformed chunk */ }
    }
  }
  return currentSessionId;
}
Phase 5Managed Identity Authentication — No API Keys
Security CriticalZero Credentials

This is the most important security step in the entire guide. Every tutorial on the internet puts the Azure OpenAI API key in an environment variable. Every one of those tutorials is teaching you to create a credential that can leak into logs, error messages, diagnostic exports, and git commits. The correct approach is Managed Identity — no key exists, therefore no key can leak.

1

Enable System-Assigned Managed Identity on the App Service

az webapp identity assign --name app-chatbot-prod --resource-group rg-chatbot-prod. Note the principalId in the output — this is the object ID you will use for RBAC assignments.

2

Assign "Cognitive Services OpenAI User" role on the OpenAI resource

This role grants the App Service permission to call the Azure OpenAI Chat Completions API using Entra token authentication: az role assignment create --assignee PRINCIPAL-ID-FROM-STEP-1 --role "Cognitive Services OpenAI User" --scope /subscriptions/SUB-ID/resourceGroups/rg-chatbot-prod/providers/Microsoft.CognitiveServices/accounts/oai-chatbot-prod. RBAC propagation takes 2–5 minutes — your first deployment attempt may return 401 if you deploy immediately after this step.

3

Assign "Key Vault Secrets User" on the Key Vault

az role assignment create --assignee PRINCIPAL-ID --role "Key Vault Secrets User" --scope /subscriptions/SUB-ID/resourceGroups/rg-chatbot-prod/providers/Microsoft.KeyVault/vaults/kv-chatbot-prod. This grants the App Service read-only access to secrets — it can read the Redis connection string but cannot create or modify secrets.

4

Disable API key authentication on the Azure OpenAI resource

Once Managed Identity is working: az cognitiveservices account update --name oai-chatbot-prod --resource-group rg-chatbot-prod --api-properties "disableLocalAuth=true". This permanently disables the API key endpoint — even if someone obtains the key value, it cannot be used. This is the enterprise security baseline: no credential-based access, Entra-only authentication.

Figure 3 — Security layers: network, identity, and data protection for production chatbot deployment
Three independent security layers — all must be configured for production🌐 Network Layer✓ HTTPS only (enforce in App Service)✓ Private endpoint for Azure OpenAI✓ VNet integration for App Service✓ Redis: SSL-only (ssl=True in conn str)✓ Key Vault: firewall + VNet rules✓ TLS 1.2+ minimum on all resources— Azure Front Door for WAF (optional)— IP restriction on App Service SCMNo public access to OpenAI or Redis🔐 Identity Layer✓ System-Assigned MI on App Service✓ disableLocalAuth on Azure OpenAI✓ Key Vault RBAC (not access policies)✓ Cognitive Services OpenAI User role✓ Key Vault Secrets User role✓ DefaultAzureCredential in code✓ No API keys in env vars or code— Conditional Access on Entra (optional)Zero credentials stored anywhere🛡️ Content & Data Layer✓ Azure OpenAI content filtering ON✓ System prompt: defines bot persona✓ Input validation (max_length check)✓ Rate limiting per session_id✓ Redis TTL: auto-expire sessions✓ Logs: no PII in App Insights traces— Azure AI Content Safety (optional)— Prompt Shield (jailbreak detection)Content filtered before user sees it
All three layers must be configured independently. A weak network layer exposes the identity layer to attack. A weak identity layer exposes content and data even if the network is secure. Configure all three before moving to production.
Phase 6Content Safety and Quota Management
Production Required429 Prevention

Azure OpenAI's built-in content filtering automatically blocks harmful content categories (hate, violence, sexual content, self-harm) on both inputs and outputs. This is enabled by default on all deployments. Your application code should handle the ContentFilterFinishReason that signals a filtered response and present an appropriate user-facing message without leaking the filter reason or the model's raw error.

Quota management prevents the 429 errors that crash production chatbots under load. Your deployment has a TPM (tokens per minute) and RPM (requests per minute) ceiling. The most reliable mitigation is exponential backoff with jitter on 429 responses combined with per-session request rate limiting to prevent any single user from consuming the entire quota.

Python — Content safety handling + exponential backoff for 429 errorsimport time, random
from openai import RateLimitError, BadRequestError

def call_openai_with_retry(messages, deployment, max_retries=3):
    for attempt in range(max_retries):
        try:
            return openai_client.chat.completions.create(
                model=deployment,
                messages=messages,
                stream=True,
                max_tokens=1024,
            )
        except RateLimitError:
            if attempt == max_retries - 1:
                raise
            # Exponential backoff with full jitter
            wait = (2 ** attempt) + random.uniform(0, 1)
            logger.warning(f"Rate limited. Retry {attempt+1} in {wait:.1f}s")
            time.sleep(wait)
        except BadRequestError as e:
            # Content filter triggered — present user-friendly message
            if "content_filter" in str(e):
                logger.warning("Content filter triggered")
                raise ValueError("I can't respond to that request.")
            raise

# Per-session rate limiting using Redis counters
def check_session_rate_limit(session_id: str, max_rpm: int = 10) -> bool:
    key = f"rate:{session_id}:{int(time.time() // 60)}" # Per-minute bucket
    count = redis_client.incr(key)
    if count == 1:
        redis_client.expire(key, 70) # 70s TTL for safety margin
    return count <= max_rpm
⚠ Quota Planning Before Going Live

Calculate your expected TPM usage before launch: Average tokens per request × Peak requests per minute = Required TPM. A typical chatbot exchange uses 500–2,000 tokens per turn (system prompt + history + user message + response). At 50 concurrent users each sending one message per minute, you need 50,000–100,000 TPM minimum. The default Standard deployment quota is 100,000 TPM. Request a quota increase via the Azure OpenAI Studio portal before you need it — approval takes 1–3 business days.

Phase 7Application Insights Monitoring
ObservabilityAzure Monitor

A chatbot without monitoring is a chatbot you cannot improve. Application Insights gives you request latency by endpoint, error rates, token usage trends, and user session analytics. Configure it from day one — retrospective instrumentation after a production incident is always more expensive than proactive setup.

1

Create Application Insights and link to App Service

az monitor app-insights component create --app appi-chatbot-prod --location eastus --resource-group rg-chatbot-prod --kind web --application-type web. Get the connection string: az monitor app-insights component show --app appi-chatbot-prod --resource-group rg-chatbot-prod --query connectionString -o tsv. Add it as an app setting: APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=...".

2

Instrument token usage as custom metrics

Log prompt tokens, completion tokens, and model latency as custom telemetry on every request. This data drives cost attribution, capacity planning, and performance dashboards. Use the opencensus-ext-azure package already in requirements.txt.

3

Create Azure Monitor alerts

Set alerts on: P95 request latency > 10 seconds (Azure OpenAI under load), error rate > 5% over 5 minutes (content filter or quota issues), and 429 error count > 0 over 1 minute (quota exhausted). Route alerts to your team's email or Teams webhook.

Python — Custom token usage telemetry with Application Insightsfrom opencensus.ext.azure.log_exporter import AzureLogHandler
from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.trace.samplers import ProbabilitySampler
from opencensus.trace.tracer import Tracer

# Configure App Insights logger
APPI_CONN = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING", "")
if APPI_CONN:
    ai_logger = logging.getLogger("ai_telemetry")
    ai_logger.addHandler(AzureLogHandler(connection_string=APPI_CONN))
    ai_logger.setLevel(logging.INFO)

def log_chat_telemetry(session_id: str, model: str,
                        prompt_tokens: int, completion_tokens: int,
                        latency_ms: float, success: bool):
    if not APPI_CONN: return
    # Log as custom event — appears in App Insights "customEvents" table
    ai_logger.info("ChatRequest", extra={
        "custom_dimensions": {
            "session_id": session_id,
            "model": model,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": prompt_tokens + completion_tokens,
            "latency_ms": latency_ms,
            "success": success
        # IMPORTANT: never log message content — contains user PII
        }
    })

# KQL query to monitor token usage in App Insights:
# customEvents
# | where name == "ChatRequest"
# | extend tokens = toint(customDimensions.total_tokens)
# | summarize total_tokens = sum(tokens), requests = count() by bin(timestamp, 1h)
# | render timechart

Terraform IaC: Full Stack in Code

Terraform — Complete chatbot infrastructure: OpenAI + App Service + Redis + Key Vault + App Insights# ── AZURE OPENAI ─────────────────────────────────────────
resource "azurerm_cognitive_account" "openai" {
  name                = "oai-chatbot-${var.env}"
  resource_group_name = var.resource_group_name
  location            = var.location
  kind                = "OpenAI"
  sku_name            = "S0"
  custom_subdomain_name = "oai-chatbot-${var.env}"
  local_auth_enabled = false # Disable API key auth — Entra only
}

resource "azurerm_cognitive_deployment" "gpt" {
  name                = "gpt-4.1-mini"
  cognitive_account_id = azurerm_cognitive_account.openai.id
  model { format = "OpenAI"; name = "gpt-4.1-mini"; version = "2025-04-14" }
  scale { type = "Standard"; capacity = 100 } # 100K TPM
}

# ── APP SERVICE ───────────────────────────────────────────
resource "azurerm_service_plan" "main" {
  name                = "asp-chatbot-${var.env}"
  resource_group_name = var.resource_group_name
  location            = var.location
  os_type             = "Linux"
  sku_name            = "P1v3"
}

resource "azurerm_linux_web_app" "chatbot" {
  name                = "app-chatbot-${var.env}"
  resource_group_name = var.resource_group_name
  location            = var.location
  service_plan_id     = azurerm_service_plan.main.id
  identity { type = "SystemAssigned" }
  site_config {
    application_stack { python_version = "3.12" }
    app_command_line = "gunicorn --bind=0.0.0.0 --timeout 600 --worker-class gevent --workers 4 app:app"
    http2_enabled = true
  }
  app_settings = {
    "AZURE_OPENAI_ENDPOINT" = azurerm_cognitive_account.openai.endpoint
    "OPENAI_DEPLOYMENT"    = "gpt-4.1-mini"
    "KEY_VAULT_URL"        = azurerm_key_vault.main.vault_uri
    "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.main.connection_string
    "SCM_DO_BUILD_DURING_DEPLOYMENT" = "true"
    # No API key — MI handles auth
  }
}

# ── RBAC ASSIGNMENTS ─────────────────────────────────────
resource "azurerm_role_assignment" "openai_user" {
  scope                 = azurerm_cognitive_account.openai.id
  role_definition_name = "Cognitive Services OpenAI User"
  principal_id         = azurerm_linux_web_app.chatbot.identity[0].principal_id
}
resource "azurerm_role_assignment" "kv_secrets" {
  scope                 = azurerm_key_vault.main.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_web_app.chatbot.identity[0].principal_id
}

Production Readiness Checklist

Azure OpenAI provisioned with custom subdomain, model deployed (gpt-4.1-mini or chosen model), and TPM capacity matching peak load estimate.
Local authentication disabled on Azure OpenAI resource (disableLocalAuth=true). No API keys used anywhere in the codebase or environment variables.
System-Assigned Managed Identity enabled on App Service with "Cognitive Services OpenAI User" and "Key Vault Secrets User" RBAC assignments.
Azure Cache for Redis provisioned with SSL-only access. Redis connection string stored in Key Vault — never in environment variables or code.
Gunicorn startup command uses gevent workers, --timeout 600, and --workers 4. Standard synchronous workers will block on streaming responses.
X-Accel-Buffering: no header set on streaming responses to disable nginx proxy buffering — required for token-by-token SSE to reach the browser without being batched.
Conversation history TTL and MAX_HISTORY_TURNS configured to prevent context window exhaustion and runaway Redis memory growth.
429 retry logic with exponential backoff and full jitter implemented. Per-session rate limiting in Redis to prevent any user consuming the entire quota.
Content filter handling catches BadRequestError with content_filter reason and returns a user-friendly message — does not expose the raw error or filter category to the user.
Application Insights connected and logging token usage (prompt + completion tokens per request) as custom telemetry. No message content logged — PII compliance.
Azure Monitor alerts configured for P95 latency, error rate, and 429 count thresholds with email or Teams delivery.
HTTPS enforced on App Service (https_only = true in Terraform). Minimum TLS 1.2 on all resources.
Health endpoint /api/health responds 200 before going live. App Service health check configured to probe this endpoint every 60 seconds.
All infrastructure in Terraform — no manual portal configurations that will drift between environments.

Key Takeaways

Never store Azure OpenAI API keys. Managed Identity with DefaultAzureCredential and get_bearer_token_provider handles authentication automatically in production, and falls back to az login locally — the same code, zero secrets.
Use Redis for conversation history, not server memory. App Service scales horizontally across multiple instances. In-memory history is destroyed on every restart and is invisible to other instances. Redis gives you durable, shared, TTL-expirable session state.
SSE streaming is not optional for production UX. Waiting 5–10 seconds for a complete response feels broken. Streaming tokens to the browser as they generate — the ChatGPT typing effect — is what users expect. Set X-Accel-Buffering: no or your nginx proxy will batch the tokens.
Use gevent workers in gunicorn for SSE. Standard synchronous gunicorn workers block on each streaming response. Gevent (async) workers handle hundreds of concurrent SSE connections on the same App Service instance.
Implement 429 handling before launch. Token quota exhaustion under load is not a theoretical risk — it is an inevitable production event. Exponential backoff with jitter prevents retry storms. Per-session rate limiting prevents any single user from exhausting the quota.
Log token usage from day one. Tokens are your primary cost driver. Application Insights custom telemetry on prompt tokens + completion tokens per request gives you the data to attribute costs by session, optimise system prompt length, and plan quota increases before hitting ceilings.

Frequently Asked Questions

How do I test the chatbot locally before deploying to App Service?
Run az login once in your terminal. DefaultAzureCredential will use your Azure CLI session to authenticate to Azure OpenAI — no environment variables or API keys needed locally. For Redis, you can run a local Redis instance with Docker: docker run -d -p 6379:6379 redis and set REDIS_CONNECTION=redis://localhost:6379 in a local .env file. Set KEY_VAULT_URL to empty string and the code will fall back to the REDIS_CONNECTION env var. The same code runs identically locally and in production — only the credential source changes.
How do I add a custom system prompt per user or department?
Store system prompt variants in Azure Key Vault or a database, keyed by user group or department. When the user authenticates (via Azure AD or your auth system), look up their department and retrieve the appropriate system prompt. Pass it as the first element of the messages array. The system prompt is not stored in Redis — it is prepended fresh on every request, so you can update it without invalidating existing sessions. For A/B testing prompts, use a flag in Redis to track which variant a session is using.
How do I add user authentication to the chatbot frontend?
Azure App Service has built-in authentication (EasyAuth) that integrates with Microsoft Entra ID, Google, Facebook, and other providers with no code changes. Enable it in the App Service → Authentication blade. Once enabled, all requests to the App Service are authenticated — unauthenticated requests are redirected to the login page. The authenticated user's identity is available in the request headers (X-MS-CLIENT-PRINCIPAL-NAME for the username). Use this to personalise the system prompt, scope conversation history to the authenticated user, and log user-level analytics.
The chatbot response cuts off mid-sentence. What is wrong?
Three most common causes: (1) max_tokens is set too low — the model runs out of its token budget before completing the response. Increase to 2048 or 4096 for detailed responses. (2) The App Service request timeout is shorter than the streaming response duration — set --timeout 600 in the gunicorn startup command and ensure the App Service's HTTP settings do not have a shorter timeout. (3) The client-side SSE reader is not correctly accumulating the [DONE] event — the stream terminated normally but the frontend stopped reading before the final token. Check your JavaScript SSE parsing logic for the [DONE] sentinel.

Popular posts from this blog

The Cloud Incumbent: AWS Bedrock Hosts Every Frontier Model and Amazon Is Betting on Neutrality AWS at $37.6 billion quarterly revenue, growing 28%. $13 billion invested in Anthropic. A $100 billion Anthropic-to-AWS commitment. Trainium with $225 billion in customer revenue commitments. The most quietly powerful AI strategy in the race. By Francis Avorgbedor | Azure Engineer  ·  July 4, 2026  ·  14 min read  ·  Amazon · AWS · Cloud AI 74 SEVENAI Momentum Score — Rank #5 $37.6B AWS Q1 2026 revenue — 28% YoY growth ▲ Fastest in 15 quarters $13B Total Amazon investment in Anthropic to date ▲ Strategic anchor 100K+ Customers running Claude on AWS Bedrock ▲ Distribution moat Amazon's AI strategy is built on a thesis that every other Magnificent Seven company is testing against — and that Amazon is uniquely positioned to win regardless of the outcome. The thesis is neutrality. In a race where Microsoft has bet on OpenAI, Google has bet on Gemini, and Meta has bet...
Performance Fix Foundry Local 1.2 Linux ARM64 Embeddings Offline ASR The Edge Latency Drop: Fixing Latency Spikes by Offloading Embeddings to Foundry Local 1.2 You are paying a full cloud round trip — network, TLS, queue, throttle risk — to turn a twelve-word search query into a vector. That is the most expensive way possible to do one of the cheapest computations in your stack. Foundry Local 1.2 now runs on Linux ARM64, which means embeddings and speech recognition can happen on a Raspberry Pi, a Jetson, or a Graviton instance — offline, unmetered, and in single-digit milliseconds. The failure signature this guide resolves # Application Insights — the embedding call, not the LLM, is your tail latency: name p50 p95 p99 calls/day POST /embeddings (cloud) 89 ms 412 ms 3,847 ms 1,240,000 POST /chat/completions (cloud) 940 ms 1,720 ms 2,910 ms 38,000 ^^^^^^^^ ...

How to Reset an Azure Virtual Machine to Factory Settings Using a Managed Disk

How to Reset an Azure Virtual Machine to Factory Settings Using a Managed Disk Azure does not have a single "factory reset" button. What it does have is something better: the OS Disk Swap — a method that swaps out the corrupted or misconfigured OS disk for a clean Windows Server managed disk without deleting the VM, its NICs, its IP addresses, or any attached data disks. Here is how it works, when to use it, and the exact steps to execute it safely. FA Francis Avorgbedor Azure Engineer July 16, 2026 15 min read Azure VMs · Windows Server · Real-World Fix 3 Methods to achieve a clean Windows Server installation on an existing Azure VM ~15min Typical OS Disk Swap duration — VM retains its NICs, IPs, and data disks throughout 0 Data disks affected by an OS Disk Swap — data disks remain attached and untouched 1 Snapshot of the original OS disk you must take before starting — no exceptions Introduction Why Azure Does Not Have a Simple Factory Reset — and What to Do Instead On a ph...

AKS CrashLoopBackOff, Pending Pods, and NotReady Nodes — The Real Fixes Engineers Use

Incident Playbook AKS Kubernetes kubectl 2026 AKS CrashLoopBackOff, Pending Pods, and NotReady Nodes — The Real Fixes Engineers Use Every AKS engineer eventually faces the same nightmare: CrashLoopBackOff at 2am, pods stuck Pending for no clear reason, or nodes flipping to NotReady mid-deployment. The difference between panic and control is knowing the exact diagnostic sequence — and the real fixes that work in production. This guide gives you both. 3 commands get pods, describe pod, and logs diagnose roughly 90% of AKS incidents before you touch anything else Exit 137 The code that means OOMKilled — the container hit its memory limit and was killed by the kernel (128 + SIGKILL 9) Events The bottom of kubectl describe is where the real cause lives — Pending, FailedScheduling, and image errors all surface there CoreDNS The single component behind most "intermittent" production failures — service discovery breaks quietly and looks like an app bug Table of Contents 01 The 3 Comm...
Can I Update My Old Computer to Windows 11 — and How Much Will It Cost? Your i7, 16GB RAM, 512GB SSD machine is powerful enough to run Windows 11 comfortably. The TPM 2.0 and Secure Boot wall is a security checkbox, not a performance ceiling. Here are two proven ways to get past it, what each one costs, and what you are trading away by doing so. $0 Cost of the Windows 11 licence if your existing Windows 10 is genuine — the upgrade remains free in 2026 2 Proven methods to bypass TPM 2.0 and Secure Boot — Rufus (easy) and Registry edit (manual) 25H2 Current Windows 11 version — all known bypass methods tested and confirmed working as of July 2026 Oct 2025 Windows 10 end of life — no more security updates. Staying on Windows 10 now carries real risk. First — Check Your BIOS Before Anything Else You Might Not Actually Need a Bypass Before running any bypass, open your BIOS and look at two settings. Many computers that fail the Windows 11 compatibility check have TPM 2.0 present in the hard...

Top 100 Best AI Tools for Software Engineers and DevOps Professionals in 2026

2026 Edition 100 Tools Software Engineering DevOps AIOps Top 100 Best AI Tools for Software Engineers and DevOps Professionals in 2026 85% of developers now regularly use AI tools. Fully AI-generated code accounts for nearly 28% of all pull requests. The question is no longer whether to use AI tools — it is which ones, in which combination, for which part of the lifecycle. This guide cuts through the noise: 100 tools, 10 categories, honest pricing, real use cases, and a selection framework for building your stack without redundancy. 85% Percentage of developers who now regularly use AI tools, per JetBrains' 2025 State of Developer Ecosystem report — up from near zero three years ago 28% Share of all pull requests containing primarily AI-generated code in 2026 — the metric that signals AI coding assistants have moved from experiment to workflow $50B Cursor's reported valuation in April 2026 Series D talks — the number that signals investor confidence in the AI developer tools ma...

Azure Files vs Azure NetApp Files: Which One Should You Choose?

Azure Files vs Azure NetApp Files: Which One Should You Choose? Performance tiers, protocol support, dual-protocol capability, pricing models, SAP/Oracle/HPC suitability, data management features, and the decision framework that maps each workload type to the right service — with step-by-step setup procedures for both. FA Francis Avorgbedor Azure Engineer July 15, 2026 20 min read Azure Storage · Architecture 4 Azure Files tiers: Premium SSD, Standard Hot, Cool, Tx Optimized 3 ANF performance tiers: Standard, Premium, Ultra — all SSD-backed 4TiB ANF minimum provisioning — significant cost floor for small workloads Dual ANF serves the same data via SMB and NFS simultaneously — AF cannot Introduction Two Services, One Surface Area — Completely Different Purposes Microsoft offers two fully managed, enterprise-grade file storage services in Azure. They share a surface area — both serve file shares over standard protocols, both run on managed infrastructure, and both integrate with Microsof...
Troubleshooting Guide AKS Kubernetes Real Solutions kubectl Azure Kubernetes Service (AKS) Troubleshooting Guide: Real Solutions to Common Problems CrashLoopBackOff at 2am. Pods stuck Pending with no obvious cause. Nodes going NotReady mid-deployment. DNS resolution silently failing in production. Every AKS engineer encounters these — the difference between engineers who panic and engineers who stay calm is knowing the exact sequence of diagnostic commands to run. This guide gives you that sequence, the root cause analysis for each failure mode, and the fix. 3 commands 90% of AKS problems are diagnosed with the same three kubectl commands: describe pod, logs --previous, and get events — in that order, every time Exit 137 The exit code that tells you everything: container killed by SIGKILL — either the Linux OOM killer (memory limit exceeded) or kubelet after grace period expired 5 min The CrashLoopBackOff ceiling: Kubernetes applies exponential backoff (10s → 20s → 40s → 80s → 160s → 3...
Planning Guide Site-to-Site VPN Gateway SKUs BGP & IPsec Azure Site-to-Site VPN Prerequisites: Network Planning, Gateway Requirements, IPsec, and Routing Half of what determines whether a Site-to-Site VPN deployment goes smoothly happens before the gateway exists at all — the subnet size, the SKU, the address space plan, and the ASN choice are all decisions that are painful or impossible to change once traffic is flowing. Get the prerequisites right, and the actual gateway creation is the boring, predictable part. Current status, verified Azure is actively consolidating VPN Gateway SKUs. The Basic SKU and the original VpnGw1–VpnGw5 SKUs are being phased out in favor of zone-redundant AZ SKUs (VpnGw1AZ through VpnGw5AZ), which Microsoft now recommends for all new deployments. After June 2026, Azure will attempt to automatically migrate remaining Standard and High Performance SKU gateways — and that migration can fail outright if the gateway subnet is too small for the target SKU. T...