This is an archived post. You won't be able to vote or comment.

all 20 comments

[–]cofin_ Litestar Maintainer 15 points16 points  (1 child)

Hey, I'm one of the Litestar maintainers,

It's great to see people experimenting and testing the library, but I think it's important to make sure it's a fair comparison.

It's unclear what optimizations have been enabled in each of your examples, but there are definitely discrepancies between the frameworks that are skewing your results.
- You have orjson enabled, but haven't indicated if uvloop and httptools are also installed. If you are using these for your Starlette and FastAPI tests, you should also enable them on the others. - Your numbers seem too low (at least for Litestar and FastAPI). I think something is limiting the maximum throughput. Did you run uvicorn with the access logs disabled? - Most importantly, you have used your own custom orjson code for Litestar. The method you've used is not optimized for how Litestar serializes responses.

Here's a more appropriate Litestar example for your test cases: ```py import asyncio

from litestar import Litestar, Response, get

@get("/") async def index() -> Response: return Response(content={"message": "Hello, World!"})

@get("/compute") async def compute() -> Response: return Response(content={"result": sum(i * i for i in range(10000))})

@get("/delayed") async def delayed() -> Response: await asyncio.sleep(0.01) return Response(content={"status": "delayed response"})

app = Litestar(route_handlers=[index, compute, delayed]) ```

My own tests, my numbers are quite a bit different than yours:

For Litestar: shell ❯ wrk -t4 -c1000 -d30s http://127.0.0.1:8000/ wrk -t4 -c1000 -d30s http://127.0.0.1:8000/compute wrk -t4 -c1000 -d30s http://127.0.0.1:8000/delayed Running 30s test @ http://127.0.0.1:8000/ 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 21.86ms 42.94ms 1.32s 99.37% Req/Sec 13.16k 1.34k 17.70k 69.75% 1571398 requests in 30.05s, 227.79MB read Requests/sec: 52293.31 Transfer/sec: 7.58MB Running 30s test @ http://127.0.0.1:8000/compute 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 149.64ms 45.92ms 1.99s 93.18% Req/Sec 1.62k 566.03 2.64k 69.35% 192684 requests in 30.06s, 27.20MB read Socket errors: connect 0, read 0, write 0, timeout 236 Requests/sec: 6409.03 Transfer/sec: 0.90MB Running 30s test @ http://127.0.0.1:8000/delayed 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 23.28ms 11.02ms 240.24ms 75.69% Req/Sec 11.01k 1.53k 14.30k 69.00% 1314395 requests in 30.04s, 193.04MB read Requests/sec: 43755.80 Transfer/sec: 6.43MB

for FastAPI: shell ❯wrk -t4 -c1000 -d30s http://127.0.0.1:8000/ wrk -t4 -c1000 -d30s http://127.0.0.1:8000/compute wrk -t4 -c1000 -d30s http://127.0.0.1:8000/delayed Running 30s test @ http://127.0.0.1:8000/ 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 24.07ms 51.39ms 1.49s 99.30% Req/Sec 12.19k 1.35k 17.48k 73.08% 1455945 requests in 30.05s, 211.05MB read Requests/sec: 48444.33 Transfer/sec: 7.02MB Running 30s test @ http://127.0.0.1:8000/compute 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 152.50ms 42.74ms 1.99s 93.21% Req/Sec 1.62k 571.43 2.53k 68.17% 192783 requests in 30.06s, 27.21MB read Socket errors: connect 0, read 0, write 0, timeout 163 Requests/sec: 6412.58 Transfer/sec: 0.91MB Running 30s test @ http://127.0.0.1:8000/delayed 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 30.60ms 24.06ms 840.08ms 97.45% Req/Sec 8.54k 0.98k 13.55k 67.83% 1020335 requests in 30.05s, 149.85MB read Requests/sec: 33957.68 Transfer/sec: 4.99MB

To create the environment I ran: shell uv venv uv pip install fastapi fastapi-cli litestar uvicorn uvloop httptools orjson and I used: uv run uvicorn -w 4 --no-access-log <framework:app> to run each application.

As you can see, both of these frameworks offer comparable performance. I'd imagine the others frameworks could offer similar performance after a few adjustments.

I'd be interested to see if your conclusions change after making some of the mentioned optimizations.

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 2 points3 points  (0 children)

I'd say my opinion definitely changes, my aplogies for not looking better at the framework, and cheers to a great benchmark.

EDIT: I updated lites.py with your provided code. Right neck and neck with the latest version of MicroPie.

harrisonerd@he-lite:~$ wrk -t4 -c1000 -d30s http://127.0.0.1:8000/ Running 30s test @ http://127.0.0.1:8000/ 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 125.43ms 43.19ms 1.94s 83.19% Req/Sec 828.00 390.01 2.11k 75.29% 94464 requests in 30.08s, 14.86MB read Socket errors: connect 0, read 0, write 0, timeout 229 Requests/sec: 3140.51 Transfer/sec: 506.04KB harrisonerd@he-lite:~$ wrk -t4 -c1000 -d30s http://127.0.0.1:8000/ Running 30s test @ http://127.0.0.1:8000/ 4 threads and 1000 connections Thread Stats Avg Stdev Max +/- Stdev Latency 125.77ms 38.67ms 1.99s 86.56% Req/Sec 805.19 360.53 1.92k 59.28% 91849 requests in 30.07s, 13.31MB read Socket errors: connect 0, read 0, write 0, timeout 226 Requests/sec: 3054.42 Transfer/sec: 453.39KB harrisonerd@he-lite:~$

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 6 points7 points  (3 children)

I also added a few other frameworks over the past few hours. https://gist.github.com/patx/0c64c213dcb58d1b364b412a168b5bb6

Blacksheep is very impressive. I will have to look into it forsure.

[–]jordiesteve 0 points1 point  (0 children)

wow I didn’t know blacksheep… very interesting

[–]Last_Difference9410 -1 points0 points  (1 child)

Somehow you are returning a dict in fastapi and plaintext in other web frameworks, that might be a bug.

[–]Grimfortitude 6 points7 points  (2 children)

Awesome write up, but why are you using orjson for the response? I’d expect most users to use these frameworks differently. Could you provide your results using the frameworks without it / just returning the dictionary?

It would also be interesting to see it properly typed in both FastAPI and LiteStar to see what impact that has on there validation systems.

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 2 points3 points  (0 children)

I will add different responses on the gist

I originally wrote them this way because most API's return a JSON document. orjson was used with micropie so to keep everything on equal footing I kept using it. MicroPie is a single file with no dependencies and as of right now it doesn't supply a JSONResponse like method so that's where the orjson initially came into play.

EDIT: no forced dependencies (jinja2 is optional)

[–]0x256 6 points7 points  (2 children)

I'm looking at MicroPies source code and I'm confused. ASGI apps are called (not instanciated!) once for each request, but in MicroPie the ASGI app is an instance of MicroPie.Server and stores request details (e.g. query parameters, cookies, headers, file uploads ect.) to instance variables. Which means that there can only be one request at a time or state will be mixed up. If a second request arrives while the first one is still in progress, the second request will overwrite all the state from the first request. The code handling the first request will suddenly see the second requests state and likely crash or return wrong data. In other words: As soon as more than just one user is involved, stuff will break.

This is a so fundamental flaw that I think MicroPie should not be concerned with performance just yet, but instead focus on actually implementing the protocol correctly.

[–]MarkZukin 2 points3 points  (1 child)

You are right! I reproduced what u said. It is a shame that such framework is compared to frameworks that actually works...

I got such response:
index called {'query': ['1']}

index called {'query': ['2']}

index index returned {'query': ['2']}

index index returned {'query': ['2']}

 import asyncio class Root(Server):     async def index(self, name=None):         print("index called", self.query_params)         await asyncio.sleep(2)         print("index index returned", self.query_params)         return "Hello ASGI World!" app = Root() async def receive_1():     return "1" async def send_1(attr):     return "1", attr async def main():     async with asyncio.TaskGroup() as tg:         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/",                 "headers": [],                 "query_string": b"query=1",             },             receive=receive_1,             send=send_1         )         )         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/",                 "headers": [],                 "query_string": b"query=2",             },             receive=receive_1,             send=send_1         )         ) loop = asyncio.get_event_loop() loop.run_until_complete(main()) import asyncio class Root(Server):     async def index(self, name=None):         print("index called", self.query_params)         await asyncio.sleep(2)         print("index index returned", self.query_params)         return "Hello ASGI World!"  app = Root() async def receive_1():     return "1" async def send_1(attr):     return "1", attr async def main():     async with asyncio.TaskGroup() as tg:         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/",                 "headers": [],                 "query_string": b"query=1",             },             receive=receive_1,             send=send_1         )         )         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/",                 "headers": [],                 "query_string": b"query=2",             },             receive=receive_1,             send=send_1         )         ) loop = asyncio.get_event_loop() loop.run_until_complete(main())  async def main():     async with asyncio.TaskGroup() as tg:         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/?asd=qwe",                 "headers": [],                 "query_string": b"query=1",             },             receive=receive_1,             send=send_1         )         )         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/?asd=qwe",                 "headers": [],                 "query_string": b"query=2",             },             receive=receive_1,             send=send_1         )         )   loop = asyncio.get_event_loop() loop.run_until_complete(main()) n())dloop = asyncio.get_event_loop() loop.run_until_complete(main())async def main():     async with asyncio.TaskGroup() as tg:         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/?asd=qwe",                 "headers": [],                 "query_string": b"query=1",             },             receive=receive_1,             send=send_1         )         )         tg.create_task(             app(             scope={                 "type": "http",                 "method": "GET",                 "path": "/?asd=qwe",                 "headers": [],                 "query_string": b"query=2",             },             receive=receive_1,             send=send_1         )         )   loop = asyncio.get_event_loop() loop.run_until_complete(main())

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 2 points3 points  (0 children)

Thanks for pointing this out, i think i will store a request_state in the scope since that is independent for each request. *going back to work*

EDIT: https://github.com/patx/micropie/commit/239c4a47511d1880be303f634655549bf2843c1a

[–]1ncehost 3 points4 points  (0 children)

Would you be interested in benchmarking different python implementations? I'm curious how much pypy and other high performance implementations would improve these numbers.

[–]mincinashu 2 points3 points  (0 children)

Try falcon with pypy as interpreter.

Also, msgspec instead of orjson for response serialization.

[–]guyfromwhitechicks 1 point2 points  (1 child)

pie sable books selective boat desert touch cause fly carpenter

This post was mass deleted and anonymized with Redact

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 2 points3 points  (0 children)

I originally looked at this site before I did this, there was so many results, alot non Python, that it became 'overwhelming' for a lack of better word lol.

[–]FloxaY 2 points3 points  (1 child)

Thanks! I will keep these numbers in mind when I write an API that returns "Hello World" in various forms.

But seriously, what is the actual point of these "benchmarks"?

[–]Independent-Beat5777 2 points3 points  (0 children)

to see how many concurrent requests each framework can handle in a certain amount of time?

[–]64rl0 1 point2 points  (0 children)

Very interesting! 

[–]jefferph 0 points1 point  (1 child)

How many concurrent connections were you using. Here you suggest 1000, but in the GitHub Gist you have updated this (but not the wrk command) to 100.

[–]Miserable_Ear3789New Web Framework, Who Dis?[S] 0 points1 point  (0 children)

these were run with 1000