mipyre
A Python web framework whose HTTP server is written in Rust and whose Python handlers run in parallel on every core, inside one process.
pip install mipyre
Python 3.14 (the regular build with the GIL). Wheels for Linux x86_64 and macOS arm64.
Why mipyre
It is fast. The server is Rust; Python runs only your handler. On 4 physical cores, a handler that returns a short string or a small dict is served at about 470,000 requests per second, with sub-millisecond latency:
| Route | Requests/s | p50 | p99 |
|---|---|---|---|
/plaintext (returns "Hello, World!") |
473,909 | 0.14 ms | 0.21 ms |
/json (returns {"message": "Hello, World!"}) |
457,791 | 0.14 ms | 0.21 ms |
It runs Python in parallel. Each core runs its own sub-interpreter with its own GIL, so CPU-bound Python handlers don't wait for each other. The same 4 cores, serving a pure-Python handler that computes for a few milliseconds per request:
| Where the handler runs | Requests/s | Server cores used |
|---|---|---|
| sub-interpreters, one per core (the default) | 1,660 | 4.0 |
a pool of 8 sub-interpreters on the multi-thread server (MIPYRE_TPC=0), which also uses the cores' SMT siblings |
1,919 | 8.0 |
the main interpreter, one GIL (runtime="main") |
414 | 1.1 |
With one GIL the server tops out at one core's worth of Python; with a GIL per core it uses all four, about 4 times as many requests.
Libraries that can't run in a sub-interpreter still run in parallel. A route declared runtime="process" runs in worker processes beside the server. A handler that takes the mean of a 10,000-element numpy array:
| Where the handler runs | Requests/s |
|---|---|
worker processes (runtime="process") |
325,601 |
the main interpreter, one GIL (runtime="main") |
77,185 |
One process. By default there is no fork and no copy of your app per core: every core's sub-interpreter lives in the same server process, and they share state through app.state. Only runtime="process" routes use extra processes.
How these numbers were measured: Linux on an AMD Ryzen 7 7840HS (8 cores, 16 threads); the server pinned to 4 physical cores and their SMT siblings, wrk on the other 4 cores and their siblings; wrk -t8 -c64 -d10s --latency without pipelining, after a 2 s warm-up, against a fresh server per run; default settings otherwise; Python 3.14.7, numpy 2.5.4; each number the median of 3 runs. The /plaintext, /json and sub-interpreter /cpu rows were measured on mipyre 3.0.0; the main-interpreter and runtime="process" rows on 2.0.0 with -c128. This measures mipyre only; it is not a comparison with other frameworks. Every mode, the raw table with min–max ranges, and the script to rerun it are in benchmarks/public/.
How it works
The Rust engine (tokio and hyper) owns the sockets, parses HTTP/1.1 and HTTP/2, enforces limits and writes responses. Python only runs your handlers.
By default the server runs one thread per physical core (per performance core on Apple silicon). Each thread owns a Python sub-interpreter with its own GIL (PEP 684), so handlers on different cores never wait for each other: one process, many GILs, no fork and no copy of your app per core. A request is accepted, parsed and answered on the same core.
Not every library can load in a sub-interpreter yet, so each route declares where it runs:
runtime= |
Where the handler runs | Use it for |
|---|---|---|
"subinterp" (default) |
the sub-interpreter of the core that took the request | pure Python, the standard library, extensions that support sub-interpreters |
"process" |
worker processes beside the server, reached through shared memory | C extensions that don't support sub-interpreters: numpy, pandas, polars, pydantic. app.state and the PostgreSQL pool are not available there |
"main" |
the server's main interpreter (one GIL) | a few stateful routes: streaming responses (Stream), async database calls, objects that must live in one place |
"isolated" |
sub-interpreters that each load their own copy of an extension | a compatibility option when process doesn't fit; uses more memory per worker |
Workers share state through app.state, a concurrent map that lives in Rust and is visible to every interpreter and core. Queues are bounded: when the server is full it answers 503 instead of growing without limit.
How to choose, and what each runtime can't do: docs/runtimes.md.
Limitations
- Libraries in sub-interpreters. An extension module loads in a sub-interpreter only if it declares support for it. The standard library and pure-Python packages do; numpy, pandas, pyarrow, orjson and pydantic don't yet, so their routes run on
processormain. What loads where. - PyO3 extensions. mipyre is built against an experimental fork of PyO3 that keeps PyO3's state per interpreter; upstream PyO3 refuses to load in a second interpreter with its own GIL. Your own PyO3 extension must be built against the fork to load in mipyre's sub-interpreters. Details.
- Per runtime.
processroutes can't beasync def, takebody=BodyStream, return aStream, or useapp.stateor the PostgreSQL pool; requests and responses there are limited to 256 KiB by default.isolatedcosts a copy of each library per worker.mainis one GIL. Details. - Platforms. CPython 3.14 with the GIL only: not the free-threaded 3.14t, not 3.13. Wheels for Linux x86_64 and macOS arm64.
- Code-only settings. A route's runtime, the app's limits, its logging and its features are set in code; no flag or variable changes them.
- No ASGI / WSGI. FastAPI, Starlette, Flask and Django apps can't be mounted yet; this is planned.
All of them, and the known issues: docs/limitations.md.
Documentation
docs/README.md lists the step-by-step codelabs and the reference pages: runtimes, configuration, testing, deployment, compatibility, integrations, observability and limitations.
Examples
examples/README.md lists the runnable apps, what each shows, the runtimes it uses and the codelab that builds it: hello world, Server-Sent Events, WebSocket, an AI agent server with MCP tools, a trading API with numpy and pandas on worker processes, and a full-stack API with validation, sessions and uploads.
Benchmarks
benchmarks/public/ has the runtime-mode benchmark above: the app, the harness, how a run is made, and every result.
Quick start
# app.py
from mipyre import Mipyre
app = Mipyre()
@app.get("/")
def index(req):
return {"message": "Hello from mipyre"}
@app.get("/hello/{name}")
def hello(req, name):
return {"message": f"Hello, {name}!"}
@app.post("/echo")
async def echo(req):
return req.json()
if __name__ == "__main__":
app.run()
python app.py # http://127.0.0.1:8000
mipyre run app:app --port 8080
mipyre dev app:app # reloads when a .py file changes
For a step-by-step tutorial (a PostgreSQL-backed fortune app, choosing runtimes, configuration, testing, production), see the fortune codelab; the other codelabs are listed in docs/README.md.
A handler takes the request first, then any path parameters by name. It returns a dict or list (JSON), str (text), bytes, or a Response(body, status_code=..., content_type=..., headers=...).
Routes that need a C extension
A sub-interpreter imports your module too, so a library that can't load there must not be imported at the top of a module the sub-interpreters import. Put those routes in a module of their own with a Router, and list it in the app:
# models.py
from pydantic import BaseModel
from mipyre import Router
router = Router(runtime="main") # or "process"
class Order(BaseModel):
ticker: str
quantity: int
@router.post("/order", body=Order)
def order(body):
return {"ticker": body.ticker, "quantity": body.quantity}
# app.py
from mipyre import Mipyre
app = Mipyre()
app.include_router("models:router")
Each runtime imports only the routers it serves. Tests serve every runtime, process routes included: docs/testing.md.
Configuration
What belongs to the app is set in code: routes and their runtimes, limits, logging, features (Mipyre(...), attributes and enable_* methods on the app). Each server setting can be given in code (ServerFlags(...) passed to app.run(), ProcessLanes(...)) and by the deployment, as a mipyre run / mipyre dev flag, a MIPYRE_* environment variable or a key in [tool.mipyre] of pyproject.toml; a deployment value overrides the code's, and the start prints what it replaced. The most used variables:
| Variable | Meaning | Default |
|---|---|---|
MIPYRE_HOST / MIPYRE_PORT |
address and port | 127.0.0.1 / 8000 |
MIPYRE_THREADS |
server threads (one sub-interpreter each) | physical cores (performance cores on Apple silicon) |
MIPYRE_TPC |
0 switches from thread-per-core to a multi-thread server with a sub-interpreter pool |
on |
MIPYRE_PROCESS_WORKERS |
worker processes for runtime="process" |
physical cores |
MIPYRE_PROCESS_TIMEOUT |
a process handler's time limit, in seconds; the worker is replaced and the client gets 504 |
30 |
MIPYRE_TLS_CERT / MIPYRE_TLS_KEY |
PEM certificate and key; turns on TLS with HTTP/2 over ALPN | unset |
MIPYRE_LOG |
1 turns on the access log and INFO-level logs; warnings and errors are always logged |
off |
MIPYRE_RELOAD |
1 turns hot reload on |
off |
Every mistake stops the server at startup, all of them at once, each naming where it came from: a value that doesn't parse, a MIPYRE_* variable or [tool.mipyre] key that is not a setting, and a value given to a server that doesn't use it.
Limits on the request path have safe defaults, set on the app: body size (2.5 MiB, 413), the handler's time (30 s, 504), and an upload's time (5 s however slowly it comes, then on while it averages at least 1 KiB/s, at most 30 s; 408); reading the request head and writing to a client that stops reading each time out after 10 s, and a silent WebSocket client is pinged, then closed.
Every setting, where it is set and its default: docs/configuration.md.
Features
- Routing: all HTTP methods, path parameters (
{id},{*rest}) injected by name, query parameters,Routermodules, a fallback route, prebuilt constant responses served without Python. - Handlers:
defandasync def;HTTPExceptionfor an error answer ({"detail": ...}, as every error the engine writes); request context (mipyre.context.ctx); request ids. - Validation:
body=for JSON bodies andquery=for query strings with pydantic;422with the reasons. - OpenAPI 3.1 document from the routes and models:
OpenApi(app, title=..., version=...). - Middleware:
before_request/after_requesthooks,on_startup/on_shutdown. - Security: cross-site requests that act refused on every route unless the site is in
enable_cors(allow_origins=...); aHostallowlist (allowed_hosts) against DNS rebinding;Cookievalues checked when made; redirects that refuse CR/LF;trusted_proxiesfor the client's address behind a reverse proxy. - Responses: gzip and brotli compression, static files with ETag /
304and precompressed.br/.gz. - Bodies and streams: multipart uploads, streaming request bodies (
body=BodyStream), Server-Sent Events from a generator (EventSourceResponse) or written by hand (Stream), WebSocket withasync defhandlers on the sub-interpreters. - Services: RPC with JSON, MsgPack or Protobuf (
@rpc(app),RPCClient); an MCP endpoint for AI agents (mcp = MCPServer(app), then@mcp.tool,@mcp.resource,@mcp.prompt). - Data: a PostgreSQL pool (
mipyre.db.PgPool), a per-worker JSON cache (@cached_json), shared state (app.state). - Operations:
/livezand/readyzprobes, Prometheus metrics, JSON or text logs, an access log with sampling (app.enable_access_log()), graceful shutdown (also from code,app.stop()), systemd socket activation, TLS, HTTP/2. - Development: hot reload,
mipyre routes; command-line flags declared where they are used (mipyre.flags);TestClientandTestServerfor integration tests of every runtime, with WebSocket support andflags.override(docs/testing.md).
Contributing
See CONTRIBUTING.md. Security issues: report them privately through the repository's Security tab.
License
Metadata
Release files for mipyre 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mipyre-3.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.14 | CPython 3.14 | Linux glibc 2.17+ x86-64 | Details for mipyre-3.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
| mipyre-3.0.0-cp314-cp314-macosx_11_0_arm64.whl | CPython 3.14 | CPython 3.14 | macOS 11.0+ ARM64 | Details for mipyre-3.0.0-cp314-cp314-macosx_11_0_arm64.whl |
Total release size: 11.3 MB
Release files / mipyre-3.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | mipyre-3.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 5.8 MB |
| Tags | CPython 3.14 Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
6713a1b1fd855a5306670342813ac2f81630b7731acf538d3296d991875fcd84
|
|
BLAKE2b-256 checksum How to use checksums |
e3332577c06ae555ceb1c288b91064eccbe54986895c2eebd86d0a08b4d54f0a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logRelease files / mipyre-3.0.0-cp314-cp314-macosx_11_0_arm64.whl
| Download URL | mipyre-3.0.0-cp314-cp314-macosx_11_0_arm64.whl |
|---|---|
| Size | 5.4 MB |
| Tags | CPython 3.14 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
96c5256c46313366948947ee000dc87b5fdcf68ee64a89f006975aaae78a1dc2
|
|
BLAKE2b-256 checksum How to use checksums |
52644139651fd27803487f07b99cf332483b690a75929a54141b3d52836eb6dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logSecurity for mipyre 3.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for mipyre-3.0.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.