Why graduate from the built-in server
mlx-lm's built-in mlx_lm.server (covered in lm.lesson6) is great for development, demos, and any single-user, low-concurrency use. The moment you need real concurrent request handling, request queueing, authentication, custom logging, or any operational concern beyond "a model that responds to OpenAI-shaped requests," you're better off wrapping mlx-lm in a FastAPI service you control.
This isn't a complicated wrapper — it's about 100 lines of FastAPI code that load a model once at startup and expose /generate and /health endpoints. The win is that you own every operational decision and can swap, fork, or extend any of them without fighting the built-in server's defaults.
The pattern
- Load the model once at startup — FastAPI's
lifespancontext manager handles this. The(model, tokenizer)pair lives in app state, reused across all requests. - Warm up — do one throwaway forward pass during startup. The first inference call after a fresh load incurs MLX's JIT compilation cost; warming up means the first real request doesn't pay this tax.
- Expose
/generate— accept a JSON body with prompt + sampling params; return generated text. Add streaming via Server-Sent Events for token-by-token delivery. - Expose
/health— a cheap endpoint your load balancer or process supervisor can hit to verify the service is alive. - Run with uvicorn — single process, single worker for MLX (concurrency comes from async I/O, not from worker pools, because the GPU is the bottleneck and you can't parallelize across workers efficiently).
What you DON'T do
Don't run multiple worker processes for the same model — they'd each load their own copy and fight for GPU memory. Don't try to parallelize generation across requests at the framework level — MLX serializes GPU access anyway, and Python's GIL plus async I/O is sufficient orchestration. Don't add a request queue at the FastAPI level for any normal traffic — uvicorn's connection handling is already a queue.
The minimum FastAPI service
The code block below is the whole service in one file. Save as app.py, run with uvicorn app:app --host 0.0.0.0 --port 8000, hit POST /generate with a JSON body. Production patterns (rate limiting, auth, structured logging) are deliberately absent — add them as you need them, but the bones are here.