ποΈ chore: remove placeholder article files β real article lives on blog.swadhin.cv
swadhinbiswas committed 2 months ago
c682305
1 parent 61b6040
commit c682305
2 files changed
+0 -500
Showing 2 changed files+0-495
βΌARTICLE.md
+0-420
| @@ -1,425 +0,0 @@ | ||
1 | -# JustAPI: a Python web framework with a Rust core β the story, the numbers, and what I actually learned | |
2 | - | |
3 | -> **Read this first.** I built JustAPI β I'm Swadhin Biswas β with agentic AI as | |
4 | -> a guide: AI helped me design, implement, debug, and document a large part of | |
5 | -> this project. I am not claiming this is the best framework in the world, and | |
6 | -> I am not claiming every number here was measured on your hardware. I am | |
7 | -> describing what I built, why I built it, what I verified, and β just as | |
8 | -> important β what I tried that did not work. The documentation site is large, | |
9 | -> much of it written with AI assistance, and not everything in it is | |
10 | -> independently verified. If you find a claim that seems wrong, it probably | |
11 | -> is. Verify it, and open an issue. | |
12 | -> | |
13 | -> **What this article is:** a technical write-up. **What it is not:** marketing. | |
14 | - | |
15 | - | |
16 | -## 1. What JustAPI is | |
17 | - | |
18 | -JustAPI is a Python web framework where the web server, the router, the TLS | |
19 | -termination, the request validation, the JSON serialization, and the database | |
20 | -access all run in Rust. Your Python code handles the application logic. That is | |
21 | -the entire premise, and it is the one sentence that explains every design | |
22 | -decision in the project. | |
23 | - | |
24 | -```python | |
25 | -from justapi import JustAPIApp | |
26 | - | |
27 | -app = JustAPIApp() | |
28 | - | |
29 | -@app.get("/") | |
30 | -def hello(): | |
31 | - return {"Hello": "World"} | |
32 | - | |
33 | -app.run() # a real server, not a dev server | |
34 | -``` | |
35 | - | |
36 | -That is a complete program. It gives you HTTP/1.1 and HTTP/2, TLS, routing with | |
37 | -path and query parameters, JSON serialization, automatic OpenAPI docs at | |
38 | -`/docs`, and a server that does not need uvicorn or gunicorn. All of it runs in | |
39 | -Rust. The Python process just runs your handler. | |
40 | - | |
41 | -The package is on PyPI (`pip install justapi`), it works on Linux, macOS, and | |
42 | -Windows, and there are wheels for the free-threaded build of CPython 3.14t. | |
43 | - | |
44 | - | |
45 | -## 2. Why I built it (the story) | |
46 | - | |
47 | -The starting point was a simple observation that most Python web frameworks | |
48 | -treat as a given: **the framework runs in Python.** FastAPI runs on Starlette, | |
49 | -Starlette runs on uvicorn, and uvicorn is a Python event loop with a lot of | |
50 | -asyncio machinery between the socket and your handler. Robyn and Granian moved | |
51 | -the *server* to Rust, but the request still crosses into Python for routing, | |
52 | -validation, and serialization. | |
53 | - | |
54 | -I asked a different question: what if almost nothing crossed into Python? | |
55 | -What if the framework β the thing that does the same work on every request β | |
56 | -lived entirely in Rust, and Python only ran the code that is different for each | |
57 | -application? | |
58 | - | |
59 | -That question shaped a project with an unusual constraint, which I wrote down | |
60 | -early and enforced throughout: **if a feature can be implemented in Rust, it | |
61 | -must be implemented in Rust.** Not "can be implemented faster in Rust" β *can | |
62 | -be implemented in Rust*. Python in the framework is reserved for the parts that | |
63 | -have to be Python: the glue between Rust values and your handlers. | |
64 | - | |
65 | -I did not do this alone. I worked with agentic AI throughout β using it as an | |
66 | -engineering guide. I made the architecture decisions; the AI helped me turn | |
67 | -them into working code, find the bugs I could not see, write the tests, and | |
68 | -keep the project honest when I wanted to skip the boring parts. There are | |
69 | -parts of this codebase I understand completely, parts I understand well enough | |
70 | -to maintain, and parts the AI wrote that I reviewed and tested but would be | |
71 | -lying if I said I could reproduce from memory. I am not hiding that; it is | |
72 | -part of how this project exists. | |
73 | - | |
74 | -The project then went through the standard engineering motions, in order: | |
75 | - | |
76 | -1. A minimal server: tokio + hyper, a PyO3 boundary, one route. Measured | |
77 | - 766k req/s on the hello-world fixture. | |
78 | -2. A native router (matchit radix trie, 51ns average lookup on 500 routes). | |
79 | -3. A memory pipeline (per-request arena, buffer pools). | |
80 | -4. Middleware in Rust: CORS, security headers, JWT, rate limiting. | |
81 | -5. Serialization in Rust (serde_json, optional simd-json). | |
82 | -6. TLS with rustls β no OpenSSL dependency. | |
83 | -7. A native API surface, replacing the ASGI shim. | |
84 | -8. Database access via sqlx: SQLite, PostgreSQL, MySQL, DuckDB. | |
85 | -9. WebSockets, SSE, background tasks, a scheduler, a plugin system. | |
86 | -10. Operational tooling: OpenTelemetry, Prometheus metrics, health checks, | |
87 | - circuit breakers. | |
88 | - | |
89 | -None of this was fast in the sense of "one weekend." It was fast in the sense | |
90 | -of "the numbers are in the repo, the tests pass, and the failures are | |
91 | -documented." | |
92 | - | |
93 | -The release history tells part of the story on its own: | |
94 | - | |
95 | -| Version | What changed | | |
96 | -|---|---| | |
97 | -| 2.0.8 | Multi-threaded tokio server, free-threaded CPython support, async handler fix (14Γ async throughput) | | |
98 | -| 2.0.9 | Native async DB awaits, Rust-native SSE, type stubs, tokenless CI publishing, first PyPI release | | |
99 | -| 2.0.10 | The CLI shipped in the wheel (it was a stub before) | | |
100 | - | |
101 | - | |
102 | -## 3. The architecture | |
103 | - | |
104 | -The request path is a pipeline, and almost every stage is Rust: | |
105 | - | |
106 | -``` | |
107 | -Kernel (epoll/io_uring) | |
108 | - β tokio connection manager (multi-threaded) | |
109 | - β TLS (rustls) | |
110 | - β HTTP parse (hyper) | |
111 | - β Router (matchit, ~51ns lookup) | |
112 | - β Middleware chain (auth, CORS, rate-limit, compression) | |
113 | - β Python boundary (zero-copy PyO3, GIL pool) | |
114 | - β Your Python handler | |
115 | - β Rust serializer (serde_json / simd-json) | |
116 | - β Response write β socket | |
117 | -``` | |
118 | - | |
119 | -The crate layout mirrors the pipeline: | |
120 | - | |
121 | -``` | |
122 | -justapi/ | |
123 | -βββ crates/ | |
124 | -β βββ justapi-core/ # networking, routing, middleware, serialization, DB pool | |
125 | -β βββ justapi-py/ # PyO3 bindings + the Python package | |
126 | -β βββ justapi-cli/ # the `justapi-cli` binary (serve, create, check, profile) | |
127 | -β βββ justapi-bench/ # benchmark harness | |
128 | -βββ python/justapi/ # pip-installable package (maturin-built) | |
129 | -βββ BENCHMARKS.md # append-only performance ledger | |
130 | -βββ DECISIONS.md # ADR log (ADR-001 through ADR-093) | |
131 | -βββ PLAN.md # roadmap with an honesty status on every phase | |
132 | -``` | |
133 | - | |
134 | -The dependency graph is intentionally small: | |
135 | - | |
136 | -``` | |
137 | -justapi-cli β justapi-core | |
138 | -justapi-py β justapi-core | |
139 | -justapi-bench β justapi-core | |
140 | -``` | |
141 | - | |
142 | -## 4. The numbers (verified, with the hardware caveat) | |
143 | - | |
144 | -All numbers below are from `BENCHMARKS.md`, measured on a single machine with | |
145 | -100 concurrent connections and 30-second runs. **Your machine will differ.** | |
146 | -What matters is the *ratio* on the same hardware, not the absolute numbers. | |
147 | - | |
148 | -| Framework | Hello-world req/s | JSON echo req/s | p99 hello-world | RSS | | |
149 | -|---|---|---|---|---| | |
150 | -| JustAPI (native fast path) | 766k | 782k | 0.48 ms | 12 MB | | |
151 | -| Granian (ASGI) | 314k | 145k | 0.74 ms | 37 MB | | |
152 | -| Robyn | 39k | β | β | β | | |
153 | -| FastAPI + Uvicorn | 36k | 33k | 24.63 ms | 29 MB | | |
154 | - | |
155 | -The "native fast path" needs two things: a route registered with `native=True` | |
156 | -and a `Schema`. In that configuration, Rust validates the body and writes the | |
157 | -response β Python never runs. Without a schema, you get the Python handler | |
158 | -path, which is GIL-bound at roughly 60β120k req/s on this hardware, same as any | |
159 | -other Python framework. | |
160 | - | |
161 | -Other verified numbers: | |
162 | - | |
163 | -- Route lookup: 51ns average on a 500-route table. | |
164 | -- Native CRUD SELECT: 181k req/s β 125Γ FastAPI on the same SQLite fixture. | |
165 | -- Native async DB awaits: 320 RPS vs 6 RPS for the blocking path on a slow | |
166 | - query (a 2M-row recursive CTE) β 53Γ. | |
167 | -- Multi-worker prefork (`justapi serve --workers 4`): 1.88Γ throughput | |
168 | - (99.7k RPS vs 53k). | |
169 | -- Free-threaded CPython 3.14t, CPU-bound Python handler: 12.4Γ vs | |
170 | - GIL-locked β the GIL ceiling disappears. | |
171 | - | |
172 | -**What I do not claim:** there is no "beats every framework on every | |
173 | -workload" claim here. Light async handlers (no I/O, just `asyncio.sleep`) sit | |
174 | -at 4β12k req/s β the asyncio loop floor β and I measured that Granian's | |
175 | -direct loop dispatch is faster on that specific shape. I do not hide it; it is | |
176 | -in the benchmarks and in the article below. | |
177 | - | |
178 | -## 5. The async story: what I tried, and what the measurements said | |
179 | - | |
180 | -This is the most important section in this article, because it is where I | |
181 | -spent the most effort proving myself wrong. | |
182 | - | |
183 | -The initial async path was bad. An async handler doing `await | |
184 | -asyncio.sleep(0.001)` served about **808 RPS** β a hard ceiling, not | |
185 | -contention. The single GIL worker was blocking on each coroutine's full | |
186 | -duration, serializing every async request. Fixing that (ADR-083) took async | |
187 | -throughput from ~800 to **11,758 RPS** β 14Γ β by making completion | |
188 | -callback-driven instead of blocking. | |
189 | - | |
190 | -Then I got ambitious. The plan was: "make the awaits Rust-native." If a | |
191 | -Python coroutine's `await` could complete on tokio instead of on the asyncio | |
192 | -loop, per-await overhead would drop, and I would beat Granian on async. | |
193 | - | |
194 | -I built four approaches and measured every one: | |
195 | - | |
196 | -| Approach | Per-await overhead | Verdict | | |
197 | -|---|---|---| | |
198 | -| `future_into_py` (Rust future wrapped as an asyncio.Future) | 2Γ slower than `asyncio.sleep` | The wrapper + cross-runtime wakeup costs more than the loop's own timer | | |
199 | -| `into_future` (coroutine β Rust future) | tied at best | Requires a running asyncio loop for task-locals; does not decouple | | |
200 | -| Hand-rolled driver, GIL attach per await | ~1125 Β΅s/await | Thread-state setup dominates | | |
201 | -| Hand-rolled driver, persistent thread | ~3.4 Β΅s/await solo | **86Γ worse concurrent** (thread per coroutine) | | |
202 | -| asyncio loop (status quo) | ~45 Β΅s/await | The only mechanism that scales | | |
203 | - | |
204 | -The multiplexing driver β one persistent Rust thread stepping many coroutines, | |
205 | -the design that should have worked β measured **16 Β΅s/await vs asyncio's | |
206 | -0.56 Β΅s/await on pure stepping** (ADR-091). The absolute floor, a bare | |
207 | -`await noop()` in asyncio, is **0.039 Β΅s**. | |
208 | - | |
209 | -The conclusion, written into the ADR log: *the Python coroutine `send()` | |
210 | -stepping is the irreducible floor, and asyncio already runs it at the minimum | |
211 | -possible rate. Any Rust wrapper around `send()` adds overhead; it cannot | |
212 | -subtract it.* A multi-loop dispatch experiment (ADR-092) also came back | |
213 | -neutral-to-worse: the GIL worker is the single dispatch point, so extra loops | |
214 | -only added GIL contention. | |
215 | - | |
216 | -I stopped. That is the part of engineering nobody writes articles about: the | |
217 | -work you delete. The async path stays on asyncio, which is the right answer, | |
218 | -and I went back to making the *operations* native instead of the coroutine. | |
219 | - | |
220 | -## 6. Native operations: where the real win is | |
221 | - | |
222 | -Since I cannot make Python coroutines faster, I made the framework operations | |
223 | -they await faster β by taking Python out of them. | |
224 | - | |
225 | -**Native async DB awaits (`query_async`).** A normal async handler doing a | |
226 | -database query would block the asyncio loop thread for the whole query. One | |
227 | -slow Postgres query froze every other async handler on the server. | |
228 | -`await app.db.query_async(...)` runs the SQL on the DB's own multi-threaded | |
229 | -tokio runtime with the GIL released. The loop is never blocked. Measured: 53Γ | |
230 | -faster than the blocking path on slow queries, and it is the only path that is | |
231 | -*safe* for real databases with network latency. | |
232 | - | |
233 | -**Rust-native SSE (`sse_native`).** A stream of events is generated entirely | |
234 | -in Rust β tokio timers, an mpsc channel, the HTTP layer drains it. Zero Python | |
235 | -per event. 100k events stream instantly. | |
236 | - | |
237 | -**`@native_async`.** A marker that routes async handlers to the fastest | |
238 | -dispatch path, and on free-threaded CPython enables true parallel dispatch | |
239 | -across workers. | |
240 | - | |
241 | -The pattern is consistent: Python declares the operation, Rust executes it, | |
242 | -and nothing in the middle is Python. | |
243 | - | |
244 | -## 7. Bugs I found in my own framework (and fixed) | |
245 | - | |
246 | -The honest part of any framework article is the bug list. Some highlights: | |
247 | - | |
248 | -**Write-path collapse.** Concurrent Python-handler writes dropped to ~3 RPS at | |
249 | -11 connections (from ~150 at 10). The request-scoped auto-transaction was | |
250 | -double-acquiring pool connections β 2N for N writes. Removed. (ADR-080) | |
251 | - | |
252 | -**GIL pool not fork-safe.** A forked child inherited the parent's initialized | |
253 | -GIL pool whose worker threads did not exist, so every Python request hung and | |
254 | -returned 504s. The pool now tracks its PID and rebuilds after fork. This was | |
255 | -also the root cause of a long-standing flaky test. (ADR-081) | |
256 | - | |
257 | -**Pydantic models as `body_schema` were broken.** `validate_body` treated a | |
258 | -Pydantic model class as a plain callable and invoked | |
259 | -`BaseModel(body_dict)`, which raised `TypeError`. This broke the single most | |
260 | -important FastAPI migration path. Fixed by validating through | |
261 | -`model_json_schema()` in the Rust engine. It shipped fixed in 2.0.9. | |
262 | - | |
263 | -**`app.run()` required an address.** The README quickstart showed | |
264 | -`app.run()` with no arguments, which crashed with a `TypeError`. Now defaults | |
265 | -to `127.0.0.1:8000`. | |
266 | - | |
267 | -**The CLI in the wheel was a stub.** The PyPI package's `justapi` command | |
268 | -printed a help message and did nothing else. It shipped fixed in 2.0.10 | |
269 | -(serve, create, check, openapi β and it delegates to the Rust CLI when | |
270 | -installed). | |
271 | - | |
272 | -**Unix-only APIs were not cfg-gated.** `UnixListener`-based functions failed | |
273 | -to compile on Windows. Gated with `#[cfg(unix)]`. | |
274 | - | |
275 | -Each of these is in the changelog with the commit that fixed it. None of them | |
276 | -was found by reading the code. They were found by running things, by | |
277 | -benchmarking, and by trying to use the framework the way a user would. | |
278 | - | |
279 | -## 8. What JustAPI ships that no other Python framework ships | |
280 | - | |
281 | -- **HTTP/3 (QUIC)** β feature-gated, serves Python handlers over QUIC through | |
282 | - the full native pipeline. As far as I know, the only Python framework with | |
283 | - a working HTTP/3 transport. | |
284 | -- **Free-threaded CPython wheels** (3.14t) with auto-detected parallel | |
285 | - dispatch. | |
286 | -- **Rust-native SSE** where the stream never touches Python. | |
287 | -- **Multi-worker prefork with auto-scaling** built into the CLI. | |
288 | -- **OIDC trusted publishing** β the release pipeline uses zero tokens; a tag | |
289 | - push builds 9 platform wheels and publishes them. | |
290 | - | |
291 | -## 9. The honest caveats (read this before using it) | |
292 | - | |
293 | -1. **The documentation site is large and much of it is AI-written.** I | |
294 | - generated a lot of the docs with AI assistance. I verified the code and | |
295 | - the benchmarks; I did not independently verify every sentence in every | |
296 | - doc page. If you rely on a doc page, test the behavior yourself. Some doc | |
297 | - pages were already found to describe APIs that did not exist, and I fixed | |
298 | - them as I found them. There are probably more. | |
299 | - | |
300 | -2. **The LLM/inference features are unverified.** Phases 41β52 of the roadmap | |
301 | - (inference engine, KV cache, continuous batching, OpenAI-compatible server) | |
302 | - are implemented but were never run with real GPU weights. The only honest | |
303 | - status is "implemented, not verified." They are not part of the release | |
304 | - story, and I do not make performance claims about them. (ADR-067) | |
305 | - | |
306 | -3. **The absolute numbers are from one machine.** Ratios matter; absolutes | |
307 | - depend on hardware, OS, and load generator. | |
308 | - | |
309 | -4. **The roadmap contains 17 "implemented but unverified" phases.** I mark | |
310 | - them π‘ in PLAN.md. That is not a typo. It means: code exists, tests pass, | |
311 | - and the real-world claim is not proven. | |
312 | - | |
313 | -5. **Async light handlers are loop-bound.** If your handler is | |
314 | - `await asyncio.sleep(0.001); return {}`, JustAPI is not faster than | |
315 | - Granian. I measured it. Use the native operations for DB/SSE/IO work. | |
316 | - | |
317 | -6. **There is no 1M RPS claim here.** The roadmap phase title once said | |
318 | - "1M RPS tuning." I never measured 1M RPS. I changed the status to | |
319 | - reflect that. (The phase overclaims; I say so in PLAN.md.) | |
320 | - | |
321 | -7. **First impressions matter, and mine were rough.** The first published | |
322 | - CLI was a stub. `app.run()` crashed without an argument. The docs had | |
323 | - fabricated API references. All fixed, but if you are evaluating the | |
324 | - project, the version history is honest about the rough edges. | |
325 | - | |
326 | -## 10. How to actually use it | |
327 | - | |
328 | -```bash | |
329 | -pip install justapi==2.0.10 | |
330 | -``` | |
331 | - | |
332 | -```python | |
333 | -# main.py | |
334 | -from justapi import JustAPIApp, native_async | |
335 | -from pydantic import BaseModel | |
336 | - | |
337 | -app = JustAPIApp() | |
338 | -app.set_database("sqlite://app.db") | |
339 | - | |
340 | -class Item(BaseModel): | |
341 | - name: str | |
342 | - | |
343 | -@app.get("/") | |
344 | -def root(): | |
345 | - return {"Hello": "World"} | |
346 | - | |
347 | -@app.get("/items/{item_id}") | |
348 | -async def get_item(item_id: int): | |
349 | - return await app.db.query_async( | |
350 | - "SELECT * FROM items WHERE id = ?", [item_id]) | |
351 | - | |
352 | -@app.post("/items", body_schema=Item) | |
353 | -def create(request): | |
354 | - data = request.json() | |
355 | - return app.db.execute( | |
356 | - "INSERT INTO items (name) VALUES (?)", [data["name"]]) | |
357 | - | |
358 | -# Rust-native SSE: no Python per event | |
359 | -app.sse_native("/events", count=1000, interval_ms=0) | |
360 | - | |
361 | -app.run() # defaults to 127.0.0.1:8000 | |
362 | -``` | |
363 | - | |
364 | -Then: | |
365 | - | |
366 | -```bash | |
367 | -python main.py | |
368 | -justapi check main.py # validate routes without serving | |
369 | -justapi serve 0.0.0.0:8080 # or use the CLI | |
370 | -``` | |
371 | - | |
372 | -For the high-performance Rust CLI: | |
373 | - | |
374 | -```bash | |
375 | -cargo install justapi-cli # provides serve --workers N --reload, create, profile | |
376 | -``` | |
377 | - | |
378 | -## 11. Where to verify everything in this article | |
379 | - | |
380 | -| Claim | Source | | |
381 | -|---|---| | |
382 | -| Benchmarks, methodology, hardware | `BENCHMARKS.md` in the repo | | |
383 | -| Architecture decisions (all 93) | `DECISIONS.md` β ADR-001 through ADR-093 | | |
384 | -| Roadmap status incl. unverified phases | `PLAN.md` (the honesty key is at the top) | | |
385 | -| Async experiments and their failure | ADR-090, ADR-091, ADR-092 | | |
386 | -| Native async DB awaits | ADR-093 | | |
387 | -| Bug fixes | `CHANGELOG.md` (2.0.8 β 2.0.10) | | |
388 | -| Code | https://github.com/swadhinbiswas/JustAPI | | |
389 | -| Docs | https://justapi.pages.dev | | |
390 | - | |
391 | - | |
392 | -## 12. The thing I actually learned | |
393 | - | |
394 | -The takeaway of this project is not "Rust is faster than Python." Everyone | |
395 | -knows that. The takeaway is more specific and more useful: | |
396 | - | |
397 | -**The performance of a Python web framework is decided by how much Python | |
398 | -runs per request β and the parts you cannot remove, you should not try to make | |
399 | -faster by rewriting them in Rust.** | |
400 | - | |
401 | -I tried to make asyncio faster by moving it to Rust. The measurements said | |
402 | -no. I made the operations *around* asyncio native instead, and that worked: | |
403 | -53Γ on slow database queries, 125Γ on native CRUD, zero-Python SSE. The | |
404 | -framework's job is to decide which parts of a request are yours and which are | |
405 | -the framework's β and then to make the framework's parts not run Python at | |
406 | -all. | |
407 | - | |
408 | -That, and the discipline to write down the failures, is what JustAPI is. | |
409 | - | |
410 | -There is a second lesson, specific to how I built this: **agentic AI is a | |
411 | -force multiplier, not a substitute for judgment.** The AI guided me through | |
412 | -Rust lifetimes, PyO3 FFI, and tokio internals I had never touched before. It | |
413 | -also wrote things that were wrong, and I only caught them because I | |
414 | -benchmarked, tested, and read the output. Use the tool; verify the work. | |
415 | - | |
416 | - | |
417 | -*Written by Swadhin Biswas, maintainer of JustAPI, with agentic AI as a | |
418 | -guide. Corrections welcome β open an issue at | |
419 | -https://github.com/swadhinbiswas/JustAPI/issues. If a number here disagrees | |
420 | -with your measurement, your measurement wins.* | |
421 | 0 | |
βΌHACKERNEWS.md
+0-75
| @@ -1,75 +0,0 @@ | ||
1 | -Show HN: JustAPI β a Python web framework where the framework itself is Rust | |
2 | - | |
3 | -I spent months building a Python web framework where almost nothing in the | |
4 | -request path is Python. Networking, TLS, routing, validation, serialization, | |
5 | -database access β all Rust. Your handler is the only Python in the pipeline. | |
6 | - | |
7 | -Why: every Python framework treats "the framework runs in Python" as a given. | |
8 | -FastAPI sits on Starlette on uvicorn, and even Rust-served frameworks like | |
9 | -Granian cross into Python for routing and serialization. I asked what happens | |
10 | -if the framework work never crosses into Python at all. | |
11 | - | |
12 | -The measured numbers (one machine, 100 concurrent connections, 30s runs β | |
13 | -ratios matter more than absolutes): | |
14 | - | |
15 | - hello-world: 766k req/s (native fast path) vs Granian 314k, FastAPI 36k | |
16 | - JSON echo: 782k req/s vs Granian 145k, FastAPI 33k | |
17 | - route lookup: 51ns on a 500-route table | |
18 | - native CRUD SELECT: 181k req/s β 125x FastAPI on the same SQLite fixture | |
19 | - native async DB: 53x faster than the blocking path on slow queries | |
20 | - memory: 12 MB RSS vs Granian 37 MB, FastAPI 29 MB | |
21 | - | |
22 | -The part I'm most proud of is what didn't work. | |
23 | - | |
24 | -I spent real effort trying to make asyncio faster by moving it to Rust β | |
25 | -driving Python coroutines from tokio. I built four approaches and measured | |
26 | -every one. The best result was 16 microseconds per await vs asyncio's 0.56 | |
27 | -microseconds on pure stepping. The absolute floor, a bare "await noop()" in | |
28 | -asyncio, is 0.039 microseconds. Python's coroutine stepping is the irreducible | |
29 | -cost, and asyncio already pays it at the minimum rate. Any Rust wrapper only | |
30 | -adds overhead. So I stopped, documented it in the ADR log (090-092), and | |
31 | -instead made the operations the coroutines await native β DB queries run on | |
32 | -Rust's tokio runtime with the GIL released, SSE streams never touch Python. | |
33 | - | |
34 | -What you get that other Python frameworks don't: | |
35 | - | |
36 | - - HTTP/3 (QUIC) through the full native pipeline (feature-gated) | |
37 | - - free-threaded CPython 3.14t wheels with auto-detected parallel dispatch | |
38 | - - Rust-native SSE β zero Python per event | |
39 | - - multi-worker prefork with auto-scaling in the CLI | |
40 | - - tokenless PyPI publishing (OIDC trusted publisher β tag push = release) | |
41 | - | |
42 | -Bugs I found in my own framework, because I benchmarked instead of assuming: | |
43 | - - concurrent Python writes collapsed to 3 RPS at 11 connections (a | |
44 | - double-acquire in request-scoped transactions) | |
45 | - - the GIL pool wasn't fork-safe β every request 504'd after a fork | |
46 | - - Pydantic models as body_schema crashed with a TypeError (broke the | |
47 | - FastAPI migration path) | |
48 | - - app.run() required an address, so the README's first example crashed | |
49 | - - the CLI shipped in the wheel was a stub; fixed in 2.0.10 | |
50 | - - Unix-only code wasn't cfg-gated; Windows didn't compile | |
51 | - | |
52 | -The honest caveats, because you will find these if you try it: | |
53 | - - the docs are large and much of it is AI-written; not every page is | |
54 | - independently verified. If a doc claims something, test it. | |
55 | - - the LLM/inference features (OpenAI-compatible server, KV cache, etc.) are | |
56 | - implemented but never run with real GPU weights. No claims. | |
57 | - - light async handlers (just asyncio.sleep, no I/O) are 4-12k req/s β the | |
58 | - asyncio floor. Granian is faster on that shape. Measured, not hidden. | |
59 | - - there is no "beats everything" claim. There is a benchmark ledger and a | |
60 | - roadmap that marks 17 phases as "implemented, not verified." | |
61 | - | |
62 | -I built this with agentic AI as a guide β it helped write a lot of the code | |
63 | -and docs, and it also wrote things that were wrong, which I only caught by | |
64 | -benchmarking and testing everything myself. Treat the AI-written parts with | |
65 | -suspicion. | |
66 | - | |
67 | -If you want the real write-up (with architecture diagram and the full async | |
68 | -failure log): https://github.com/swadhinbiswas/JustAPI/blob/main/ARTICLE.md | |
69 | - | |
70 | -PyPI: https://pypi.org/project/justapi/ | |
71 | -Repo: https://github.com/swadhinbiswas/JustAPI | |
72 | -Benchmark ledger: https://github.com/swadhinbiswas/JustAPI/blob/main/BENCHMARKS.md | |
73 | -ADR log (all 93 decisions, including the failures): https://github.com/swadhinbiswas/JustAPI/blob/main/DECISIONS.md | |
74 | - | |
75 | -Swadhin Biswas | |
76 | 0 | |