Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Creating Own LLM›Serving LLMs: APIs, APIs, Microservices, and Scalable Inference

Deployment and Advanced Topics

Serving LLMs: APIs, APIs, Microservices, and Scalable Inference

This lesson covers how to serve large language models (LLMs) efficiently by exposing them via APIs, structuring them as microservices, and scaling inference to handle high demand. It matters because raw model performance is useless if users can't access it reliably, and scaling ensures cost-effective deployment as traffic grows. You reach for these techniques when moving from local prototyping to production, where latency, availability, and resource efficiency become critical.

Why Serve LLMs as APIs?

Serving LLMs as APIs decouples the model from the applications that use it, enabling modularity and reusability. When you expose an LLM via an API, multiple clients—web apps, mobile apps, or other services—can interact with the model without needing to understand its internals or manage its dependencies. This separation also allows you to update or swap the model independently of the client applications, reducing downtime and maintenance overhead. APIs also provide a standardized interface, which simplifies integration and ensures consistent behavior across different use cases. For example, an API can enforce rate limits, authentication, and input validation, protecting the model from misuse or overload. Without an API, every client would need to embed the model, leading to bloated applications, redundant resource usage, and difficulty in scaling. By centralizing the model behind an API, you also gain better observability, as you can log requests, monitor performance, and debug issues in one place. This approach is foundational for production deployments, where reliability and maintainability are as important as raw model accuracy.

# A minimal FastAPI endpoint to serve an LLM
def load_model():
    # In practice, load your LLM here (e.g., from Hugging Face or a checkpoint)
    # For this example, we'll simulate a model with a simple function
    return lambda prompt: f"Processed: {prompt}"

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
model = load_model()

class PromptRequest(BaseModel):
    text: str
    max_tokens: int = 50

@app.post("/generate")
async def generate(request: PromptRequest):
    try:
        # Simulate model inference
        response = model(request.text)
        return {"response": response}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# Run with: `uvicorn main:app --reload`
# Test with: `curl -X POST "http://localhost:8000/generate" -H "Content-Type: application/json" -d '{"text":"Hello"}'`

Designing a Scalable API Endpoint

A scalable API endpoint must handle concurrent requests efficiently while minimizing latency. The key challenge is that LLMs are computationally expensive, and naive implementations can quickly become bottlenecks under load. To address this, you should design your endpoint to be stateless, allowing any instance of the service to handle any request. This enables horizontal scaling—adding more servers to distribute the load. Additionally, use asynchronous processing to avoid blocking the API while waiting for the model to generate responses. For example, you can offload inference to a background task queue (like Celery or Redis Queue) and return a job ID immediately, letting the client poll for results later. This pattern is especially useful for long-running inferences. Another critical aspect is input/output validation: ensure the API rejects malformed requests early to avoid wasting resources. Finally, implement rate limiting to prevent abuse and ensure fair usage. Without these measures, a single user could monopolize the service, degrading performance for everyone else. Scalability isn’t just about handling more requests—it’s about doing so predictably and fairly.

# Scalable FastAPI endpoint with async processing and rate limiting
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import asyncio

app = FastAPI()
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

# Allow CORS for frontend clients
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["POST"],
    allow_headers=["*"],
)

# Simulate async model inference
async def async_model_inference(prompt: str) -> str:
    await asyncio.sleep(1)  # Simulate processing time
    return f"Async response to: {prompt}"

@app.post("/generate")
@limiter.limit("5/minute")  # Rate limit to 5 requests per minute per IP
async def generate(request: PromptRequest, req: Request):
    try:
        response = await async_model_inference(request.text)
        return {"response": response}
    except RateLimitExceeded:
        raise HTTPException(status_code=429, detail="Too many requests")

# Run with: `uvicorn main:app --reload`
# Test with: `ab -n 10 -c 2 -p post.json -T "application/json" http://localhost:8000/generate`

Microservices: Isolating LLM Components

Microservices break down the LLM serving system into smaller, independent services, each responsible for a specific function. For example, you might separate the API gateway, model inference, logging, and monitoring into distinct services. This isolation improves fault tolerance: if the logging service fails, the inference service can continue running. It also allows you to scale components independently—if inference is the bottleneck, you can add more inference servers without touching the API gateway. Microservices also enable technology diversity; you can use the best tool for each job, such as Python for inference and Go for high-throughput logging. However, this approach introduces complexity in service discovery, inter-service communication, and data consistency. To mitigate this, use lightweight protocols like gRPC or REST for communication, and implement retries with exponential backoff to handle transient failures. Containerization (e.g., Docker) and orchestration (e.g., Kubernetes) are essential for managing microservices at scale. Without proper isolation, a monolithic architecture can become a single point of failure, where a bug in one component crashes the entire system. Microservices force you to design for failure, which is critical in production environments.

# Dockerfile for a microservice (inference service)
# Build with: `docker build -t llm-inference .`
# Run with: `docker run -p 8000:8000 llm-inference`

FROM python:3.9-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Install system dependencies for some LLMs
RUN apt-get update && apt-get install -y \
    gcc \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

---
# requirements.txt
fastapi==0.95.2
uvicorn==0.22.0
torch==2.0.1
transformers==4.30.2

---
# main.py (same as previous FastAPI example, but now containerized)

Scaling Inference with Load Balancing

Load balancing distributes incoming requests across multiple instances of your LLM service, preventing any single instance from becoming overwhelmed. This is essential for handling traffic spikes and ensuring high availability. A load balancer sits between clients and your servers, routing requests based on factors like server health, current load, or geographic proximity. For LLMs, you might use a round-robin approach for simplicity, or a least-connections strategy if inference times vary significantly. However, load balancing alone isn’t enough—you must also ensure your model can run on multiple servers without state conflicts. This means avoiding in-memory caching of model weights or session data; instead, use shared storage (like Redis) for transient data and load the model independently on each server. Another challenge is cold starts: if a new server is added to the pool, it may take time to load the model, leading to latency spikes. To mitigate this, pre-warm servers by loading the model before adding them to the pool. Without load balancing, a single server failure could take down your entire service, and scaling would require manual intervention. Load balancing automates this process, making your system resilient and self-healing.

# Nginx load balancer configuration for LLM inference servers
# Save as `nginx.conf` and run with: `nginx -c /path/to/nginx.conf`

events {
    worker_connections 1024;
}

http {
    upstream llm_servers {
        # List of inference server IPs/ports
        server 192.168.1.10:8000;
        server 192.168.1.11:8000;
        server 192.168.1.12:8000;
        
        # Least connections strategy
        least_conn;
    }
    
    server {
        listen 80;
        
        location / {
            proxy_pass http://llm_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

# Test with: `ab -n 100 -c 10 http://localhost/generate`

Advanced: Dynamic Batching for Efficiency

Dynamic batching groups multiple inference requests into a single batch, allowing the LLM to process them in parallel on a GPU. This leverages the fact that modern GPUs excel at parallel computation, and batching can significantly reduce the per-request latency and cost. For example, processing 8 requests in a batch might take only 20% longer than processing one request, effectively increasing throughput by 4x. However, dynamic batching requires careful tuning: if you wait too long to form a batch, individual requests experience higher latency; if you batch too aggressively, some requests may be delayed unnecessarily. To implement this, use a queue to collect incoming requests and a background thread to form batches at fixed intervals (e.g., every 100ms). The batch size should be chosen based on your GPU’s memory and the model’s requirements—larger batches consume more memory but improve throughput. You’ll also need to handle cases where a batch isn’t full, such as padding inputs to the same length or using masking. Without dynamic batching, each request is processed sequentially, leading to underutilized GPUs and higher costs. This technique is especially valuable in multi-tenant environments, where you need to serve many users efficiently.

# Dynamic batching with PyTorch and FastAPI
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from fastapi import FastAPI, BackgroundTasks
from typing import List
import time
import asyncio

app = FastAPI()
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").to("cuda")
request_queue = asyncio.Queue()
BATCH_INTERVAL = 0.1  # 100ms
MAX_BATCH_SIZE = 8

async def process_batches():
    while True:
        batch = []
        # Collect requests for BATCH_INTERVAL seconds
        start_time = time.time()
        while time.time() - start_time < BATCH_INTERVAL and len(batch) < MAX_BATCH_SIZE:
            try:
                # Wait for a request with a small timeout
                request = await asyncio.wait_for(request_queue.get(), timeout=0.01)
                batch.append(request)
            except asyncio.TimeoutError:
                continue
        
        if batch:
            # Tokenize and pad inputs
            inputs = tokenizer([req["text"] for req in batch], return_tensors="pt", padding=True).to("cuda")
            with torch.no_grad():
                outputs = model.generate(**inputs, max_length=50)
            
            # Decode and return responses
            for i, req in enumerate(batch):
                response = tokenizer.decode(outputs[i], skip_special_tokens=True)
                req["future"].set_result(response)
        
        await asyncio.sleep(0.001)  # Yield control

@app.on_event("startup")
async def startup_event():
    asyncio.create_task(process_batches())

@app.post("/generate")
async def generate(text: str, background_tasks: BackgroundTasks):
    future = asyncio.Future()
    await request_queue.put({"text": text, "future": future})
    response = await future
    return {"response": response}

# Run with: `uvicorn main:app --reload`
# Test with: `ab -n 20 -c 5 -p post.json -T "application/json" http://localhost:8000/generate`

Key points

  • Serving LLMs as APIs decouples the model from clients, enabling modularity, reusability, and easier maintenance across multiple applications.
  • Scalable API design requires statelessness, asynchronous processing, and rate limiting to handle concurrent requests efficiently and fairly.
  • Microservices isolate LLM components, improving fault tolerance and allowing independent scaling of different parts of the system.
  • Load balancing distributes traffic across multiple servers, preventing bottlenecks and ensuring high availability during traffic spikes.
  • Dynamic batching groups inference requests to leverage GPU parallelism, reducing latency and cost per request in multi-tenant environments.
  • Cold starts in load-balanced systems can be mitigated by pre-warming servers before adding them to the pool to avoid latency spikes.
  • Input validation and rate limiting protect the API from misuse, ensuring consistent performance and preventing resource exhaustion.
  • Containerization and orchestration tools are essential for managing microservices at scale, simplifying deployment and scaling operations.

Common mistakes

  • Mistake: Deploying an LLM as a monolithic API without microservices. Why it's wrong: This creates a single point of failure, makes scaling difficult, and increases latency for users. Fix: Break the LLM serving into microservices (e.g., tokenization, inference, post-processing) to improve fault tolerance and scalability.
  • Mistake: Ignoring batch processing for inference requests. Why it's wrong: Processing requests one-by-one underutilizes GPU/TPU resources and increases costs. Fix: Implement dynamic batching to group similar requests and maximize hardware efficiency.
  • Mistake: Not optimizing model quantization for production. Why it's wrong: Full-precision models consume excessive memory and compute, slowing down inference. Fix: Use quantization (e.g., FP16, INT8) to reduce model size and improve throughput without significant accuracy loss.
  • Mistake: Overlooking cold-start latency in serverless deployments. Why it's wrong: Serverless functions may spin down, causing delays when the LLM is reloaded. Fix: Use warm-up strategies or containerized deployments to keep the model ready for inference.
  • Mistake: Failing to monitor token-level latency and throughput. Why it's wrong: End-to-end latency metrics hide inefficiencies in specific stages (e.g., token generation). Fix: Track per-token latency and throughput to identify bottlenecks in the inference pipeline.

Interview questions

What is the primary purpose of serving a large language model (LLM) via an API, and why is this approach beneficial?

The primary purpose of serving an LLM via an API is to provide programmatic access to the model's capabilities, allowing users to integrate its functionality into applications without needing direct access to the underlying infrastructure. This approach is beneficial because it abstracts complexity, enabling developers to focus on building features rather than managing model deployment. APIs also facilitate scalability, as they can handle multiple requests concurrently, and they support security by allowing controlled access through authentication and rate limiting. For example, an API endpoint like `/generate` can accept input text and return model predictions, making it easy to embed LLM capabilities in web or mobile apps.

How would you design a simple REST API for an LLM using a framework like FastAPI? Provide a basic code example.

To design a simple REST API for an LLM using FastAPI, you would create an endpoint that accepts input text, processes it with the model, and returns the generated output. The API should include request validation, error handling, and asynchronous processing for scalability. Here’s a basic example: ```python from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class TextRequest(BaseModel): text: str @app.post('/generate') async def generate(request: TextRequest): try: # Assume `model` is a pre-loaded LLM object response = model.generate(request.text) return {'output': response} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) ``` This code defines a `/generate` endpoint that takes a JSON payload with a `text` field, processes it, and returns the model's output. FastAPI automatically handles JSON serialization and request validation, making it a great choice for serving LLMs.

Why might you choose to serve an LLM as a microservice instead of a monolithic API, and what are the trade-offs?

Serving an LLM as a microservice is advantageous because it decouples the model from the rest of the application, allowing independent scaling, deployment, and updates. For example, if the LLM requires frequent retraining or optimization, a microservice can be updated without affecting other parts of the system. This approach also improves fault isolation—if the LLM service fails, it won’t crash the entire application. However, microservices introduce trade-offs, such as increased complexity in managing inter-service communication, latency due to network calls, and the need for robust monitoring. For instance, a monolithic API might handle requests faster but becomes harder to maintain as the system grows. Microservices are ideal for large-scale systems where modularity and scalability are priorities.

Explain how you would implement load balancing for an LLM inference service to handle high traffic. Include a conceptual example.

To implement load balancing for an LLM inference service, you would deploy multiple instances of the model behind a load balancer, which distributes incoming requests evenly across the instances. This ensures no single instance is overwhelmed, improving throughput and reducing latency. For example, you could use a tool like Nginx or a cloud-based load balancer (e.g., AWS ALB) to route requests. Here’s a conceptual setup: 1. Deploy the LLM service on multiple servers or containers. 2. Configure the load balancer to use a round-robin or least-connections algorithm. 3. Monitor instance health and automatically remove unhealthy nodes. 4. Scale horizontally by adding more instances during traffic spikes. For instance, if you have 3 LLM instances, the load balancer ensures each handles roughly 33% of requests. This approach is critical for maintaining low latency and high availability under heavy load.

Compare batch processing versus real-time inference for serving LLMs. When would you use each, and why?

Batch processing and real-time inference serve different use cases for LLMs. Batch processing involves processing large volumes of input data in bulk, often offline, which is ideal for tasks like generating reports, summarizing documents, or pre-computing embeddings. It’s efficient for high-throughput scenarios where latency isn’t critical, as it minimizes overhead by reusing model states across multiple inputs. For example, processing thousands of customer reviews overnight to generate insights. Real-time inference, on the other hand, processes requests immediately, making it suitable for interactive applications like chatbots or live translation. It prioritizes low latency but requires more resources to handle concurrent requests. Use batch processing for cost-effective, large-scale tasks and real-time inference for user-facing applications where responsiveness is key.

Describe how you would optimize an LLM inference service for low latency and high throughput. Include specific techniques and tools.

Optimizing an LLM inference service for low latency and high throughput requires a combination of model optimization, hardware acceleration, and architectural improvements. First, quantize the model to reduce its size and computational requirements, using techniques like 8-bit or 4-bit quantization to speed up inference without significant accuracy loss. Second, leverage hardware accelerators like GPUs or TPUs, which are designed for parallel processing and can handle large matrix operations efficiently. Third, implement model caching to store frequent predictions and avoid redundant computations. Fourth, use asynchronous processing and batching to maximize GPU utilization—for example, grouping multiple requests into a single batch to process them in parallel. Finally, deploy the service on a high-performance framework like TensorRT or ONNX Runtime, which are optimized for inference. For instance, TensorRT can optimize the model graph and fuse layers to reduce latency. Combining these techniques ensures the service handles thousands of requests per second with minimal delay.

All Creating Own LLM interview questions →

Check yourself

1. Why is dynamic batching critical for serving LLMs efficiently?

  • A.It reduces the model's memory footprint by compressing weights.
  • B.It groups multiple inference requests to maximize GPU utilization and throughput.
  • C.It converts the model to a smaller architecture to speed up inference.
  • D.It caches frequent responses to avoid recomputing them.
Show answer

B. It groups multiple inference requests to maximize GPU utilization and throughput.
The correct answer is that dynamic batching groups requests to maximize GPU utilization. The other options are incorrect: weight compression (option 0) is unrelated to batching, architecture conversion (option 2) is model distillation, and caching (option 3) is a separate optimization.

2. What is the primary trade-off when quantizing an LLM for production?

  • A.Higher memory usage but faster inference.
  • B.Lower accuracy but reduced computational cost.
  • C.Longer training time but better generalization.
  • D.Increased model size but lower latency.
Show answer

B. Lower accuracy but reduced computational cost.
The correct answer is that quantization reduces computational cost (e.g., memory, compute) at the potential expense of accuracy. The other options are incorrect: quantization reduces memory usage (option 0), doesn't affect training time (option 2), and reduces model size (option 3).

3. How do microservices improve the scalability of LLM serving?

  • A.By combining all components into a single API endpoint.
  • B.By allowing independent scaling of components like tokenization and inference.
  • C.By reducing the model's parameter count to fit on smaller GPUs.
  • D.By caching all responses to avoid recomputation.
Show answer

B. By allowing independent scaling of components like tokenization and inference.
The correct answer is that microservices enable independent scaling of components. The other options are incorrect: combining components (option 0) is the opposite of microservices, reducing parameters (option 2) is model pruning, and caching (option 3) is unrelated to microservices.

4. What is a key challenge of serving LLMs in a serverless environment?

  • A.Cold-start latency due to model loading delays.
  • B. Inability to handle batch requests.
  • C. Higher cost compared to containerized deployments.
  • D. Limited support for GPU acceleration.
Show answer

A. Cold-start latency due to model loading delays.
The correct answer is cold-start latency, as serverless functions may spin down and reload the model. The other options are incorrect: serverless can handle batches (option 1), costs vary (option 2), and GPUs are supported (option 3).

5. Why is per-token latency monitoring important for LLM serving?

  • A.It ensures the model generates tokens in the correct order.
  • B.It reveals inefficiencies in specific stages of the inference pipeline.
  • C.It reduces the total number of tokens the model can generate.
  • D.It guarantees the model will never produce incorrect outputs.
Show answer

B. It reveals inefficiencies in specific stages of the inference pipeline.
The correct answer is that per-token latency monitoring reveals inefficiencies in stages like token generation. The other options are incorrect: token order (option 0) is unrelated to latency, reducing tokens (option 2) is not a goal, and correctness (option 3) is not guaranteed by monitoring.

Take the full Creating Own LLM quiz →

← PreviousModel Compression: Quantization, Pruning, and Knowledge DistillationNext →Optimizing Inference: ONNX, TensorRT, and Model Optimization Tools

Creating Own LLM

36 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app