Hyperlambda vs FastAPI: An Honest, Reproducible Benchmark

Hyperlambda vs FastAPI: An Honest, Reproducible Benchmark

I published a benchmark claiming Hyperlambda was 20× faster than FastAPI, and the criticism it received was fair.

No pinned versions. A home-grown load client. An untuned Python endpoint written to lose. And worst of all, a payload mismatch in my first run — 25 records against the full table. That last one is not a methodology quibble, it is a wrong number, and it was mine.

So I rebuilt the whole thing the way a hostile reader would build it.

The 20× replicated almost exactly. I also found the run where Hyperlambda is only twice as fast, and that one is in the table too.

ConfigurationRequests in 30 sReq/secp99 latency
FastAPI, "as typically written" (sync + SQLAlchemy ORM, 1 worker)4,269142692 ms
FastAPI, tuned (async + raw SQL, 1 worker)5,804193450 ms
FastAPI, "as typically written", 4 workers23,699787440 ms
FastAPI, tuned, 4 workers30,7451,022324 ms
Hyperlambda (Magic), Debug build, JWT auth on every request84,8942,000–2,823238 ms

Everything below is copy-paste reproducible. Prove me wrong.

Methodology

Machine. MacBook Pro, Apple M3 Pro, 18 GB RAM. Every target on the same machine, run sequentially, with a 5-second warm-up discarded before each 30-second measured run.

Load generator.wrk from Homebrew, 4 threads, 50 connections, keep-alive.

This detail deserves its own paragraph, because it is where my first attempt at the rebuild went wrong too. I started with ApacheBench, and every single server came back at roughly 500 req/s — FastAPI, Hyperlambda, everything. That is not a result, that is a measuring instrument reporting its own limit. If your load generator cannot out-run your fastest target, your benchmark is not measuring your servers. It took me an embarrassing while to notice, and it is the most common way benchmarks quietly lie.

Versions, pinned. Python 3.14.4, FastAPI 0.141.1, uvicorn 0.52.3, SQLAlchemy 2.0.52, aiosqlite 0.22.1. Magic Cloud running locally from its repository.

Workload. The Chinook SQLite database, SELECT ArtistId, Name FROM Artist — 275 rows. Same database file for every target.

The payload check that killed version one

Before measuring anything, I verified that every endpoint returned byte-identical 13,011-byte payloads. Same rows, same JSON, same shasum.

This is the check whose absence invalidated my original article, so it now runs first, and nothing gets measured until it passes.

curl -s http://127.0.0.1:8000/artists | python3 -m json.tool | shasum

Every way this is unfair, in both directions

Unfair to Python: the Hyperlambda endpoint performs JWT authentication and RBAC authorization on every single request. The FastAPI endpoints perform no authentication of any kind. I am comparing a secured endpoint against unsecured ones, and the secured one wins anyway.

Unfair to Hyperlambda: it ran in a Debug development build. Production Magic deployments are Release builds. The numbers here are a floor, not a ceiling.

Fair to Python, deliberately: I benchmarked it naive and tuned, at 1 worker and 4. The tuned version has no ORM anywhere near the hot path — async handler, raw SQL over aiosqlite, a small connection pool. It is the version a good Python engineer writes when they know they are being measured.

What the numbers actually say

Against FastAPI as it is typically written, Hyperlambda is 14–20× faster.

Sync def, SQLAlchemy ORM, one uvicorn process. This is not a straw man — it is how an enormous amount of production FastAPI is genuinely written, because it is what the tutorials teach and what works fine until it does not. Against it: 2,000–2,823 req/s versus 142. The original claim survives the rebuild.

Tuning Python closes some of the gap, and then the gap stops closing.

Async plus raw SQL takes the single-worker number from 142 to 193 req/s — Hyperlambda still 11–15× ahead. Scaling to four uvicorn processes takes it to 787–1,022 — Hyperlambda still 2.0–3.6× ahead.

That last figure is the honest floor of this article and I want it read clearly: against maximally tuned FastAPI running four worker processes, Hyperlambda is roughly twice as fast, not twenty times. Anyone quoting my 20× without that sentence attached is quoting me badly.

It is also worth being precise about what "scaling to four workers" means in Python: forking four entire interpreter processes and multiplying the memory footprint to get there. Hyperlambda's number is one process.

And the curiosity my critics will enjoy most.

The naive 4-worker setup (787 req/s) nearly matched the tuned 4-worker setup (1,022). All that async plumbing, all that ORM removal, and process count did most of the work.

Python performance tuning is far less obvious than Python performance criticism.

What the remaining gap actually buys

Here is the part I care about more than the ratio.

That Hyperlambda endpoint was not written. It was generated in about 30 seconds by clicking a button — or by an AI agent, from a single sentence. It arrived with authentication and authorization already enforced. It required no deploy step, no container, no restart. And in a Debug build, on a laptop, while checking a JWT on every request, it served 2,000+ requests per second — over 170 million requests per day.

There is no small or medium business workload on Earth that needs more than that.

So the question was never whether a meta-platform can beat a hand-tuned stack at throughput. The question is which bottleneck you actually have. If it is requests per second, you already know it, and you have a team working on it. For almost everyone else the bottleneck is the three weeks a developer needed to build the endpoint in the first place — which is the argument I made with receipts in A Complete CRM in One Conversation.

Throughput is cheap. Developers are not.

What this benchmark does NOT prove

One endpoint, one table, one read query, on localhost, on a single machine. It says nothing about writes, joins, large payloads, concurrent mutation, or anything crossing a network.

The Python numbers are specific to this stack. FastAPI on uvicorn with SQLAlchemy is not all of Python, and other stacks will land elsewhere.

And it was run by me, the author of Magic. Vendor benchmarks are guilty until replicated. So replicate it — and publish what you get, especially if it contradicts me.

Reproduce it

Get a Chinook SQLite database, then create these two files.

naive.py — FastAPI as it is typically written:

# "As typically written" FastAPI: sync def + SQLAlchemy ORM.
from fastapi import FastAPI, Depends
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session

DATABASE_URL = "sqlite:///./chinook.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


class Artist(Base):
    __tablename__ = "Artist"

    ArtistId = Column(Integer, primary_key=True, index=True)
    Name = Column(String, nullable=False)


app = FastAPI()


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.get("/artists")
def get_artists(db: Session = Depends(get_db)):
    artists = db.query(Artist).all()
    return [{"ArtistId": a.ArtistId, "Name": a.Name} for a in artists]

tuned.py — no ORM, async, raw SQL, pooled connections:

# "Maximally tuned" FastAPI: async def + raw SQL via aiosqlite,
# small connection pool, no ORM anywhere near the hot path.
import asyncio
import aiosqlite
from fastapi import FastAPI

DB = "./chinook.db"
POOL_SIZE = 10

app = FastAPI()
_pool = None


@app.on_event("startup")
async def startup():
    global _pool
    _pool = asyncio.Queue()
    for _ in range(POOL_SIZE):
        db = await aiosqlite.connect(DB)
        await _pool.put(db)


@app.on_event("shutdown")
async def shutdown():
    while not _pool.empty():
        db = await _pool.get()
        await db.close()


@app.get("/artists")
async def get_artists():
    db = await _pool.get()
    try:
        cursor = await db.execute("SELECT ArtistId, Name FROM Artist")
        rows = await cursor.fetchall()
        await cursor.close()
        return [{"ArtistId": r[0], "Name": r[1]} for r in rows]
    finally:
        await _pool.put(db)

Then run all four configurations:

# Setup
python3 -m venv venv && ./venv/bin/pip install fastapi uvicorn sqlalchemy aiosqlite
cp chinook.db .   # same DB file for all targets

# Start servers
./venv/bin/uvicorn naive:app --port 8000               # naive, 1 worker
./venv/bin/uvicorn tuned:app --port 8001               # tuned, 1 worker
./venv/bin/uvicorn tuned:app --port 8002 --workers 4   # tuned, 4 workers
./venv/bin/uvicorn naive:app --port 8003 --workers 4   # naive, 4 workers

# Verify payload equality FIRST — all must return identical 13,011 bytes / 275 rows
curl -s http://127.0.0.1:8000/artists | python3 -m json.tool | shasum

# Measure: 5s warm-up (discarded), then 30s measured
wrk -t4 -c50 -d5s   > /dev/null
wrk -t4 -c50 -d30s --latency 

# Magic — note the JWT, because this endpoint enforces RBAC
wrk -t4 -c50 -d30s --latency -H "Authorization: Bearer " \
    "http://localhost:5000/magic/modules/chinook/artist?limit=-1"

Magic is MIT-licensed and open source. The repository is at github.com/polterguy/magic, with documentation at docs.ainiro.io.

If your numbers disagree with mine, publish them. That is the entire point of writing this one down twice.