AI Gateway Architecture

Build a centralized AI gateway for routing, rate limiting, cost tracking, and model fallback across multiple LLM providers.

An AI gateway sits at the heart of a production AI system, routing inference requests, managing model lifecycle, and coordinating between services. It matters when multiple models serve different parts of an application, when request patterns vary widely (batch vs. real-time), and when models must be versioned, monitored, and scaled independently. It is the single point of truth for AI services, transforming raw inputs into AI-powered outputs with consistency, observability, and resilience.

Request Routing and Model Selection

The gateway receives incoming requests and dispatches them to appropriate models. Each request carries metadata—model_version, priority, timeout, features—that guide routing decisions.

For a real-time inference request, a client sends a JSON payload over HTTP:

POST /predict HTTP/1.1
Host: ai-gateway.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yVqojrYHDijq
{
  "input": "What is the capital of France?",
  "model_version": "v2.1",
  "features": {
    "context_length": 128,
    "embedding_size": 768
  }
}

The gateway matches the model_version and features against a routing table stored in a database. The routing table is a PostgreSQL table with columns: model_name, version, deployment_id, load_balancer_strategy, thresholds.

SELECT deployment_id, load_balancer_strategy
FROM model_routing
WHERE model_name = 'text-classifier'
  AND version = 'v2.1'
  AND (context_length >= 128 AND embedding_size >= 768);

The gateway uses the load_balancer_strategy (round_robin, least_connections, consistent_hashing) to pick a deployment. If no match is found, it defaults to the latest stable version.

A common failure mode: requests fail silently when model_version is misspelled. The gateway logs model_version not found: v2.1 but returns HTTP 500 with no body. The root cause is a mismatch between the version string in the request and the database, where v2.1 is stored as v2.1.0. The solution is to normalize all version strings during registration.

Model Lifecycle Management

Models are registered with the gateway before they can be used. Registration is a two-step process: first, the model is uploaded; second, it is deployed.

Model upload uses a standard multipart/form-data request:

POST /models/register HTTP/1.1
Host: ai-gateway.example.com
Authorization: Bearer <admin-token>
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="model_file"; filename="model.onnx"
Content-Type: application/octet-stream

<binary data of model.onnx>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
  "name": "text-classifier",
  "version": "v2.1",
  "framework": "onnx",
  "input_schema": {
    "type": "string",
    "max_length": 1024
  },
  "output_schema": {
    "type": "array",
    "element_type": "string",
    "max_elements": 10
  },
  "dependencies": [
    "numpy==1.24.0",
    "onnxruntime==1.14.0"
  ],
  "min_resources": {
    "cpu": 2,
    "memory": "4Gi",
    "gpu": 1
  },
  "default_timeout_ms": 500,
  "metrics": [
    "latency_p95",
    "throughput_qps",
    "error_rate"
  ]
}
------WebKitFormBoundary7MA4YWxkTrZu0gW--

Upon successful upload, the gateway stores the model in a shared object store (e.g., S3) and updates the models table:

INSERT INTO models (
  name, version, file_uri, metadata, created_at, status
) VALUES (
  'text-classifier', 'v2.1', 's3://models/text-classifier/v2.1.onnx',
  '{"framework": "onnx", "input_schema": {"type": "string", "max_length": 1024}, ...}',
  '2024-05-12T08:30:00Z',
  'registered'
);

The gateway then triggers a deployment job via a message queue (e.g., RabbitMQ). The job consumes the model, builds a Docker image, and registers it with a container orchestration system.

Deployment is asynchronous. A client can poll /models/{name}/{version}/status:

GET /models/text-classifier/v2.1/status HTTP/1.1
Host: ai-gateway.example.com
Authorization: Bearer <client-token>

Response:

{
  "model_name": "text-classifier",
  "version": "v2.1",
  "status": "deploying",
  "progress": 0.67,
  "logs": [
    "Building image: python:3.10-slim",
    "Copying model file to container",
    "Running onnxruntime test suite",
    "Starting service on port 8000"
  ],
  "last_updated": "2024-05-12T08:35:12Z"
}

A sharp edge: if the gateway receives a request before deployment completes, it returns HTTP 503 Service Unavailable with a Retry-After header. The client must handle Retry-After: 15 and backoff. Misconfiguring Retry-After leads to repeated client timeouts, especially when deployment takes 20 seconds but the gateway sets Retry-After: 5.

Request Transformation and Preprocessing

The gateway applies preprocessing pipelines to inputs before sending them to the model. This includes input validation, feature extraction, and format conversion.

A request to /predict may include a preprocessing_pipeline field:

{
  "input": "What is the capital of France?",
  "model_version": "v2.1",
  "preprocessing_pipeline": [
    "normalize_text",
    "tokenize",
    "embed_with_bert"
  ]
}

The gateway checks the preprocessing_pipeline against a registry of available pipelines stored in a JSON table:

SELECT pipeline_id, steps, dependencies, description
FROM preprocessing_pipelines
WHERE name = 'text-classifier-preprocess'
  AND version = 'v1.0';

Each step is a function that transforms the input. The gateway applies them sequentially:

  1. normalize_text: converts input to lowercase, removes extra whitespace, corrects punctuation.
  2. tokenize: splits text into tokens using a tokenizer (e.g., bert-base-uncased).
  3. embed_with_bert: runs a BERT model to produce embeddings.

The transformed input is passed to the model as a dictionary:

{
  "input_ids": [101, 2026, 2044, 102, 2124, 102],
  "attention_mask": [1, 1, 1, 1, 1, 1],
  "sequence_length": 6
}

If a step fails, the gateway logs the error and returns HTTP 502 Bad Gateway with a detailed error message:

{
  "error": "Preprocessing pipeline failed",
  "details": {
    "step": "embed_with_bert",
    "error_code": "model_load_failed",
    "message": "Failed to load BERT model from /models/bert-large-uncased/1.0.onnx",
    "timestamp": "2024-05-12T08:40:03Z",
    "retryable": true
  },
  "pipeline_steps": [
    "normalize_text",
    "tokenize",
    "embed_with_bert"
  ]
}

A silent failure: tokenize step uses a tokenizer trained on a different vocabulary than expected. The gateway logs Tokenizer mismatch: expected vocab size 30522, got 30512. The client sees correct output but with suboptimal results—no one notices until a model drift alert fires.

Versioning and A/B Testing

The gateway supports multiple model versions in parallel. It routes requests based on version and traffic weights.

To enable A/B testing, the gateway supports a traffic_split configuration:

{
  "experiment_id": "ab-test-2024-05",
  "model_versions": {
    "v2.1": 0.7,
    "v2.2": 0.3
  },
  "routing_strategy": "weighted_random",
  "metadata": {
    "variant_a": "v2.1",
    "variant_b": "v2.2",
    "target_accuracy": 0.91
  }
}

The gateway assigns each incoming request to a version with the specified probability. The routing_strategy determines how the choice is made:

The gateway tracks A/B test metrics via a experiment_metrics table:

INSERT INTO experiment_metrics (
  experiment_id, version, request_id, latency_ms, success, model_output, timestamp
) VALUES (
  'ab-test-2024-05',
  'v2.1',
  'req-12345',
  124,
  true,
  '{"label": "Paris", "confidence": 0.94}',
  '2024-05-12T08:45:01Z'
);

When a new version is promoted, the gateway updates the traffic_split and notifies downstream services via a POST /experiments/{id}/promote call. The promotion is idempotent: calling it multiple times with the same experiment_id and version updates only the active version.

A failure mode: traffic_split is updated but the gateway does not propagate the change immediately. Clients see version v2.1 for 10 minutes after v2.2 is promoted. The fix is to use a cache layer (e.g., Redis) with a TTL of 30 seconds, and to invalidate the cache on every traffic_split update.

Observability and Health Checks

The gateway exposes a health endpoint at /health:

GET /health HTTP/1.1
Host: ai-gateway.example.com

Response:

{
  "status": "healthy",
  "timestamp": "2024-05-12T08:50:00Z",
  "uptime_seconds": 123456,
  "services": {
    "model_registry": {
      "status": "healthy",
      "latency_ms": 23,
      "details": {
        "database_connection": "up",
        "cache_hit_rate": 0.87
      }
    },
    "deployment_scheduler": {
      "status": "healthy",
      "pending_jobs": 12,
      "last_job_completed": "2024-05-12T08:49:58Z"
    },
    "preprocessing_engine": {
      "status": "warning",
      "latency_ms": 112,
      "details": {
        "last_error": "Failed to load tokenizer from /models/tokenizers/2024-04-15.json",
        "error_count": 4
      }
    }
  },
  "active_models": [
    "text-classifier:v2.1",
    "image-classifier:v1.3"
  ],
  "error_logs": [
    {
      "timestamp": "2024-05-12T08:48:10Z",
      "level": "error",
      "message": "Failed to load model file from S3: s3://models/text-classifier/v2.1.onnx",
      "correlation_id": "c-123456"
    }
  ]
}

Clients use this endpoint for liveness and readiness probes. The gateway also exposes metrics via Prometheus:

A common failure: a model version is registered but not yet deployed. The health check returns status: healthy, but active_models lists only deployed models. The gateway must poll the deployment system every 30 seconds to update active_models.

Error Handling and Retry Policies

The gateway implements retry policies per model and request type. A request may fail due to model unavailability, timeout, or input schema mismatch.

On failure, the gateway returns a standard error structure:

{
  "error": "ModelInferenceFailed",
  "code": "model_inference_failed",
  "message": "Inference request failed for text-classifier:v2.1",
  "details": {
    "model_version": "v2.1",
    "input_schema": {
      "expected": {
        "type": "string",
        "max_length": 1024
      },
      "actual": {
        "type": "array",
        "length": 3
      }
    },
    "error_type": "schema_mismatch",
    "retriable": true,
    "retry_after": 5000,
    "correlation_id": "c-789012"
  },
  "timestamp": "2024-05-12T08:55:30Z"
}

The gateway retries failed requests using exponential backoff. The retry policy is configured per model:

{
  "model_name": "text-classifier",
  "version": "v2.1",
  "retry_policy": {
    "max_retries": 3,
    "base_delay_ms": 100,
    "max_delay_ms": 2000,
    "backoff_factor": 1.5,
    "retry_on": [
      "timeout",
      "network_error",
      "schema_mismatch"
    ],
    "ignore_errors": [
      "model_not_found",
      "version_not_available"
    ]
  }
}

If a request fails after 3 retries, the gateway sends it to a dead-letter queue (e.g., Kafka) for manual inspection. A consumer service processes the dead-letter queue, logs errors, and sends alerts via email or Slack.

A sharp edge: the gateway does not handle timeout correctly when the model service returns HTTP 200 but with a large Content-Length and slow streaming. The gateway considers the request successful even if the model takes 8 seconds and the gateway timeout is 5 seconds. The fix is to use read_timeout and body_timeout separately, and to validate that the entire response body is received before marking the request as complete.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.