12 requests per second: A realistic look at Python web frameworks
suade.org
suade.org
Sure, the framework matters at very large scale and the benefits from optimizing the framework become large when you're doing millions of requests a second over many thousands of servers because it can help reduce baseline cost of running the service.
But I agree with the author's main point which seems to be that framework performance is pretty meaningless when comparing frameworks if you're just starting on a new project. Focus on making a product people wanna actually use first. If you're lucky enough to get to scale you can work about optimizing it then.
A simple example is, say, foreign keys. Trying to access the foreign key of an object by doing `book.user.id` does an additional query for the user table to get the ID. It's less known that the id is immediately available by just doing `book.user_id` instead.
I've spent time optimising things like text searches down from 2000+ queries to about ~4, and one of the more noticeable things to me isn't actually the number of joins, rather the SELECT's that take place. Many of these ORMs do a SELECT * unless you explicitly tell them to otherwise, and when dealing with large-ish datasets or on models that have large text fields this translates into significant time taken to serialise these attributes. So you can optimise the query and still have it take a long time until you realise that limiting the initial `SELECT` parameter is probably more efficient than limiting the number of joins.
I personally love tools like Miniprofiler [1] for this (though maybe there's something better today, it's been a while since I've worked on that type of thing). It's a constant and accessible way to keep an eye on what goes into each request, and I've caught many of those bad queries before they were problems by using it (eg: "WTF, why did it take 9 queries and 250ms to grab what looks to be a single row from a single table?!").
So without a large number of rows it can be hard to know what it will actually do.
https://docs.djangoproject.com/en/dev/topics/testing/tools/#...
Hibernate (on Java) at least optimizes this specific use-case. At first, accessing a lazy-loaded property-object will give you a "proxy" and you can access the ID without incurring a database load (since it knows that anyway). And when doing a query, the object won't be joined when requesting book.user.id unless it needs to be (like you have some other WHERE clause that requires an actual join on that row).
> Trying to access the foreign key of an object by doing `book.user.id` does an additional query for the user table to get the ID. It's less known that the id is immediately available by just doing `book.user_id` instead.
But that's not really unsuspecting. `book.user` is asking for the user table, `book.user_id` is not. Those two things are not identical even though they return the same value.
I don't think that's right. There is a user_id column in the book table, so why shouldn't there be book.user_id?
hibernate is then "the magic "environment where it just works"
I realize OO is out of fashion now but it's still true and it still works and I've been in a lots of projects where ORM was useful
Hmm.. Sounds like a bug. Why is this not the same value for a foreign key?
Now perhaps some ORM's prefer to be thinner, to provide more footguns via a leaky abstraction that mixes implementation details with the object mapping. I don't think those are good implementaations.
To me, it seems like the (hypothetical?) implementation you're talking about is much more leaky and footgun-y than the more straightforward ("thinner", in your words) version. In order for foo.user.id to not execute a new query, foo.user would have to return some sort of proxy object that only fetched the user row when you tried to access a field that hasn't been loaded. That's way more magic than the more obvious solution—which is to load the row when you access the related object—and could easily cause more problems than it solves in the long run when you need to debug very specific queries.
Furthermore, how is going out of your way to hide a field that exists in the database (user_id) not the definition of a leaky abstraction? What purpose does it serve to direct you through an unnecessary layer if all you need is the ID?
Or you can just consider user_id to be a reference pointer that is part of foo, while user.id is an attribute of user. Totally different things and I am glad that the distinction is there.
If it's a bug, sounds like a "wontfix" to me.
A similar concept called covered queries exists whereby you index a table by foreign key, and a few additional columns that you do not expect to use in join conditions, but you do expect to frequently retrieve. Depending on your database, requests for only columns in the index (some being the join condition, some subsequent columns being in the set of popular additional columns) means faster access to those popular columns only. In the context of ORMs, you would need to do something to avoid a default behavior of "select *" in order to exploit this index.
Not necessarily, it can be overridden: "id" is only the default for models that haven't explicitly been given a field with the "primary_key" kwarg (common on legacy tables where the primary key column might for example be "user_id").
The alias guaranteed to be the same value is ".pk", and I'm not sure what django does if you try to create a column named "pk" that isn't the primary key.
Django does provide relatively easy ways to get over the N+1 issue, though. If you do Book.objects.select_related('user'), only one query is made.
You still need to understand a minimum of SQL and databases, and usually those that complain about the ORM are the ones that expect it to be a "sufficiently advanced compiler", but it has matured so much that nowadays the developers consider a *bug* every time the answer to How do I do this query X? involves something along the lines of use .extra or raw sql.
Even though Django has come a long way, I greatly prefer ORMs like SQLAlchemy and Ecto that map more closely to the SQL query I'm trying to write.
`user` would be a property defined as a User object on the Book model, so accessing `book.user` will cause the framework to fetch the entire user model (even if we then only fetch the id).
On the other hand, `book.user_id` is the auto-generated database column, generated to make the above property definition possible. But since this `user_id` is directly defined on the book object, there is no need to query the user table.
SQL is readable (at least far more than 20m lasigna of boilerplate objects decorated with tons of annotations googled from internet where no one really know what they do) and you KNOW that if you have optimized the database structure (and filesystem, and network,... :D) and SQL statement you will get peak performance while with ORMs you are on constant hunt what else can you turn on while they are far too huge to read their code.
And they are becoming quite absurd after they "mature" and begin adding corner cases that no one has thought about when they were a starting project.
public interface UserDao {
@SqlUpdate("CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR)")
void createTable();
@SqlUpdate("INSERT INTO user(id, name) VALUES (?, ?)")
void insertPositional(int id, String name);
@SqlQuery("SELECT * FROM user ORDER BY name")
@RegisterBeanMapper(User.class)
List<User> listUsers();
}
This is great because it's explicit. No hidden queries. context
.update(User.USER)
.set(User.USER.NAME, userName)
.where(User.USER.ID.eq(userId))
.execute()For small queries with straight forward joins, a query builder is nice and readable.
But for larger, more complex queries, I found putting the query into its own file was best for readability.
And to me, SQL is the easiest language to learn and read of all, and while I understand doing basic marshalling of SQL records into objects is tedious, its really not hard at all, and it just saves so so much heartache down the road.
The exception might be very basic CRUD apps that are meant to be used by third parties and want to support multiple backends like mysql/postgres/whatever. There might be other exceptions as well where you are just trying to prototype/find market fit, but for a typical project that you know is going to be used in a real way, the risks just don't outweigh the benefits IMHO.
Some of the team griped a bit, but I absolutely think it was the right move in hindsight. At least once a year I would hear about an outage due to an ORM gone wild, and these outages were usually prolonged by the fact that things function, and then there is finger pointing between the DB/DBAs and the app developers, etc.
I'd love to have a tool that just generates an object type for a given SQL query's result rows, and a function signature for its query parameters.
For a similar purpose in Python I generally use psycopg with namedtuple resultsets; the namedtuple 'records' do what I need for the returned data and are reasonably efficient.
If you want to keep going forward with it, I'd recommend working on making it clearer what the examples do, and looking at sqlc for Go for inspiration. Of the existing versions of this people replied with, that one had the clearest API, or at least clearest explanation on its landing page.
We don't need ORM, we need OQM (object-query mapping)!
So now my pattern is to use for example ActiveModel in Ruby for the models, but not ActiveRecord for the persistence part.
Also, the big thing is you won't know how to translate to other ORMs if you don't know SQL. Eg once you know that you want an index, it's a matter of a web search to find out what the syntax is in your ORM. But if you just started with ORM, you might not realize that kind of thing is part of how it works.
plus it means you don't have to write all the dumb "select * from books where bookName = :bookName" code that obviously can be handled trivially by the ORM. You just use SQL the places where it makes sense.
the ORM hate always strikes me as a little misplaced because of this - you can always write SQL where it's appropriate, in any decent ORM. And you can write bad queries in SQL too. Obviously very complex queries are maybe better reserved for raw SQL but it seems like a lot of the hate comes from maybe less experienced engineers getting in over their head with complex work items because ORMs "make it easy", and that is going to happen with raw SQL too if you throw those same engineers at those work items.
ORMs aren't inherently that heavyweight, I see people complaining here about SQLAlchemy and as a Java developer I don't have performance concerns about hibernate. That sounds to me like a Python problem and a "this specific ORM isn't performant" problem, not ORMs being bad as a whole. And if you really want a "just load the data for me and do nothing else that incurs a performance hit" approach then you can use stateless objects and it's just a wrapper around the DB to load and transform the data for you and/or do a raw, whole-object update back to the DB.
The reason is that sure, for your first 10 basic select queries, the ORM saved you half an hour. Then you got to that complicated join and had to resort to looking up archane syntaxes and prototyping attribute quirks for an hour, when the junior guy got the whole thing written in 15 minutes of trial/stackoverflow/error in a SQL prompt.
Then, even when the "expert" did get it working, guess who is going to be the one looking at it again when trying to figure out production support issues? The junior guy, who is now clueless and has to spend two hours to figure out what this crazy ORM mess does here. The better alternative was just to have the SQL there ready to go so it is well understood and can be ran against the production database or a test database to reproduce the issue. No questions whatsoever.
I have seen this over and over again, more than a statistically relevant number of times.
Maybe we work in very different fields, but "basic select queries" makes up 90% of what I need to fetch from the database.
If I'm working on a forum and I want to load user 123, with all their posts, all the awards each post has, and the count of friends the user has, with Eloquent (Laravel's ORM), I could do:
`User::with('posts.awards')->withCount('friends')->findOrFail(123);`
That would return me a User model, with a collection "posts" containing a list of Post models, each with a collection "awards" of Award models, and a field `friends_count` with the number of friends. It would run three queries: one to fetch the user, one to fetch the posts, and one to fetch the awards. Depending on how I have configured my models, I can have things like dates automatically hydrated to DateTime objects.
Compare that to plain SQL queries; I would have to fetch the users, including manually writing the subquery for the friend count. Once I had those users I would then have to fetch the posts, and then again for the awards. If I want them in a hierarchy like the ORM example gives me, I then need to loop through each set of records and manually stitch them together. Not difficult, but super tedious.
Sure a complicated join is better done with as little magic as possible, but Eloquent exposes functions for adding subselects, joins, etc. in a way that just reads like SQL (and maps 1:1 underneath).
But the thing is, a lot of things aren't really on the hot path and optimizing them isn't worth a ton of time. Like yeah so what if this web request that only gets used 2% of the time makes 5 extra database requests that it shouldn't, on a single data item. Not gonna tank the overall program.
I guess I'd accept that it's important to be aware of what you're pulling, regardless of whether that's automagically when a proxy object sees it needs to be lazy loaded, or explicitly in a query. Throwing junior developers on performance-critical paths is going to be a problem anywhere and on any DB access layer.
Some years ago, one morning I gave a co-worker a recommendation on how to improve a loop that was unnecessarily hitting database through the Django ORM. He committed the fix that afternoon. Barely an hour later I accidentally reintroduced the exact same slowdown in the exact same loop when adding a different piece of data to it.
Soooo yeah, ORMs can be so simplistic it's too easy to do by accident even if you know exactly what's going on under the hood.
And like I said above, usually you don't just select out the entire contents of a table and handle it in the application, so I reflexively did the wrong thing as well, because of how easy it was to do with Django's ORM.
``` [book.author.name for book in Book.objects.all()] ```
The way I sped it up was to call `.values()` on the query, which serializes the data into a dict and prevented me from accidentally making subsueqent calls.
PS: Indent by 4 spaces for code formatting.
For instance, Django has prefetch_related and select_related. At almost every Django conference, there's a talk on this topic because it's so important and very underused/overlooked. But these are provided methods of the ORM.
Aside from that, there are wonderful introspection tools such as django-debug-toolbar to view the raw SQL and its performance.
It can be argued that if a solution written in Django hasn't had its database performance introspected with for instance django-debug-toolbar, then the solution isn't done. This is a small step with big rewards.
This introspection can easily identify where raw SQL is useful. But apply it late in process: As a project matures, the costs of converting some queries into raw/hybrid SQL are lower, as the statement is less likely to change. But keep these SQL statements in the models and managers, don't let them spill into views, template tags etc.
I've got a couple of web handlers that after quite a bit of work I can legitimately claim will run on the microsecond timeframe... but they're the exception. Generally even a single DB hit across the network, even on the same system, is going to blow right past the web framework's time you're using.
On that note, using this sort of metric, japronto's claimed results smell funny. Even with a 4GHz processor, getting 1,214,440 requests per second on a single core is ~3300 cycles per request. That's less than one cycle per byte in the HTTP request for a reasonable request (with no blocking on any sort of memory request), and that's not counting the TCP itself, any response, or the overhead of switching back and forth between C and Python. I can't see how this is possible without a huge degree of corner cutting; just validating that what you've received is a legal HTTP request, correctly encoded, decoding the fields, etc. is going to eat into that pretty fast, even with all the SSE instructions you may be able to throw at it. (And to emphasize, I'm not saying this is "impossible", just that it requires a lot of corner cutting. I've also got a "web server" out in the wild that handles "web requests" blazingly fast... because it basically ignores the entire web request and shovels out a hard-coded response. Very fast. Not a very good web server.)
This seems like a false dichotomy. Avoiding obvious performance mistakes such as the ones you mentioned does not require additional focus that would detract from general building. It just requires that you know what you are doing.
If you are the type of person who makes said mistakes, its unlikely you would ever go back and fix them by "focusing" on performance because the issue is simply that you don't know what you don't know. Likely someone else will come along in future and point out your mistakes to you.
Optimization that actually hinders you from building and requires focus is at the very margins and almost no one is going to those levels in typical "application" code.
It feels like a sensible advice but "optimization", if ever possible, can only get you so far until you need a costly refactoring or rewrite in my experience.
As projects can be very different in context, it is all about what makes a minimal implementation "viable".
Completely agree and this has been my experience ae well. To this I'll also add, inadequate thought put into data modelling. One would have to think lesser about query performance or cost of overfetching if data is modelled around the needs of the system it would serve instead of just modelling real life entries and their relationships as is, straight onto the database.
* aiosql[0] to write raw SQL queries and having them available as python functions (discussed in [1])
* asyncpg[2] if you are using Postgres
* Map asyncpg/aiosql results to Pydantic[3] models
* FastAPI[4]
Pydantic models become the "source of truth" inside the app, they are designed as a copy of the DB schema, then functions receive and return Pydantic models in most cases.
This stack also makes me think better about my queries and the DB design. I try to make sure each endpoint makes only a couple of queries. Each query may have multiple CTEs, but it's still only a single round-trip. That also makes you think about what to prefetch or not, maybe I want to also get the data to return if the request is OK and avoid another query.
[0] https://github.com/nackjicholson/aiosql [1] https://news.ycombinator.com/item?id=24130712 [2] https://github.com/MagicStack/asyncpg [3] https://pydantic-docs.helpmanual.io/ [4] https://fastapi.tiangolo.com/
I was using aio-pika for the rabbit queue and asyncpg and was getting a consistent 25k messages/sec for like 200 lines of code.
Do you have an example project which uses all of these I could look at?
Then you could use dataclasses and map them to the database via sqlalchemy.
https://github.com/adsharma/dataclasses-sql
Couple of other techniques to speedup python:
* Transpile python to another language (py2many) * Compile a large graphql like query to a single query plan in python which can be accelerated. (Fquery)
Both projects on my github.
That would have the benefit of the standardized api of an ORM and the flexibility of SQL, without the coupling.
- because you have all your queries written in the same place, when you create a db migration, you know where to look for code to update, and can even perform some automatically.
- sql queries could be turned into a stored procedure with a simple marking, without changing any code.
Of course the downside is that dynamically exploring data is not as easy as with an ORM.
Each asyncpg result has a dictionary-like interface, so I can convert it to a Pydantic model easily.
If I know an endpoint is going to be hit hard, I forgo trying to use the ORM (except to maybe get the table name from the model obj so some soul can trace it's usage here in the future) and directly do an engine.execute(<raw query>). Makes a huge difference. Next optimization I do is create stored procedures on the database. Only then I start thinking about changing the framework itself.
For folks like me who want to get prototypes off the ground in hours, flask and fastapi are godsend, and if that means I have to worry about serving thousands of requests a second soon thats a happy problem for sure.
That sounds to me like a Python problem and a "this specific ORM isn't performant" problem, not ORMs being bad as a whole. Python has never been the fastest language (it's far slower than, say, Java) and the GIL really prevents applications from scaling well without multiple instances.
And if you really want a "just load the data for me and do nothing else that incurs a performance hit" approach then you can use stateless objects and the ORM truly becomes just a wrapper around the DB to load and transform the data into an object for you and/or do a raw, whole-object update back to the DB.
They're clunky monstrosities that act only as guard-rails for inexperienced developers. Far better to invest a few days (which is realistically all you need) to improve their SQL skills and/or code-review practices.
ORMs are there for mapping objects and well, relations. Most ORMs provide additional features, but at the core they can, and for more complicated queries they should be used with native SQL queries. They are made for OLTP not for OLAP
There are "simple ORMs" that only map results of SQL queries to objects. They do not provide a magic query API - which is the source of most problems. I don't do Python, but for .NET there is Dapper https://github.com/StackExchange/Dapper, you can have a look what I mean. You write the SQL query, explicitly execute it, the library maps the results of that query into objects (it's C#, so you have to declare the class. In Python I'd imagine it would create the object for you)
In the case of SQLAlchemy (referenced in the article), many people use it as a database connection abstraction rather than an ORM. It's kinda the equivalent of "ODBC" for certain Python libraries like Pandas.
For instance, in Pandas you can write your dataframe to the database by going `df.to_sql(tblname, sqlachemycon)` where sqlalchemycon is the connection instance to any database that SQLAlchemy supports.
https://pandas.pydata.org/pandas-docs/version/0.23.4/generat...
I use SQLAlchemy for this purpose alone and write straight up SQL. I've never used the ORM parts of SQLAlchemy.
If you connect the IDE to a database it starts to recognize the SQL to the point it behaves like rest of the code (you have autocomplete etc). I am starting to think that this is the correct approach and ORMs were just a hack trying to achieve that.
The exceptions are SQL elements that cannot be (easily, or at all) parametrized, such as the column lists in SELECT, ORDER BY, GROUP BY, or changing the WHERE "shape" and so on.
In my experience, this tends to be a minority of queries, although an important minority.
----
P.S. If string concatenations are your bottleneck, then your database is screaming fast! The real-life bottlenecks are usually in excessive database round-trips and unoptimized query plans, and are orders-of-magnitude larger.
> I think there is a fairly high performance cost to doing so many string concatenations every time the code runs
String concatenation is extremely cheap compared to any sort of IO or computation.
The article has a link to https://techspot.zzzeek.org/2015/02/15/asynchronous-python-a..., but failed to mention the key takeaway from the article:
> threaded code got the job done much faster than asyncio in every case
In the end you want to measure perf with and without SSL so you can identify the real bottleneck. Otherwise you might just benchmark your SSL implementations handshake performance and not what you really want.
As an example, I found that Kubernetes nginx-ingress can’t cache SSL sessions on the upstream side (nginx to your app). So request bursts can really hurt your application unless your pool has enough open connections to handle the burst (keep alive and keep alive requests). Without benchmarking my app in different ways I wouldn’t have figured this out as easily and might have just assumed my app was slow.
But IME real-world bottlenecks have more to do with overall architecture. People tend to heavily focus on technical details, such as concurrency architecture--the how. But the biggest opportunities for improved performance usually involve functional aspects of an architecture--the what. (Note that these aren't fixed categories; they're relative positions. A technical detail often becomes a functional model as development progresses.)
12 RPS is a long way from implicating SSL. If you get to the point where SSL is an identifiable bottleneck, you've either made a series of tremendously good decisions or exceptionally poor decisions.
I don't think that's right. Cloudflare's blog [1] says they can do about 9.5x the handshakes/sec with ECDSA at 256-bits vs RSA at 2048. Verification for ECDSA signatures are somewhat slower, but it's usually an acceptable tradeoff to make clients do a bit more work so that servers do a lot less.
I agree though, at 12 RPS, TLS isn't the bottleneck.
[1] https://blog.cloudflare.com/ecdsa-the-digital-signature-algo...
And then Pyston was re-released...and changed everything. It was drop in compatible for us and we saw a 50% drop in latencies.
Source availability aside, I suggest anyone running CPython in prod take a look.
I remember pyston v1 from Dropbox. You are speaking about v2, which is a binary package (closed-source at the moment)?
We're very happy with it. Great compatibility, no horrendous warm up times, and very meaningful speedups.
Not much more to say. It's the same, just a bit faster.
You're probably done rapid prototyping by this point anyway.
I don't want to start a language war, but maybe Python is not the first choice for their requirements.
- JavaScript still feels messy
- C/C++ is complex, but it is often offset by the complexity/needs of the project (e.g. in embedded)
- Java has become kind of bloated with all the new stuff
So as others have already mentioned, development productivity is in many cases far more important than speed of code. In my ~20y long career I have rarely seen a project failure due to runtime performance. Most of them failed due to speed/agility of development iterations and also project/product management issues (bad fit, unrealistic project plan, lack of focus and customer feedback).
That said, if I need to look for another language due to performance, that would probably be Rust.
I feel about the same amount of grievances with both. For instance I dislike Python's async and functional semantics (list(map(lambda n...). But it has a much better standard library overall, a lot of the scripting syntax semantics (file opening, requests, etc.) feel cleaner, etc.
I'm more versed in JS but as the knowledge curves converge in months or years to come I don't think I'll be significantly more productive in Python.
I know you were expressing an entirely subjective opinion, probably contingent on the amount of day-to-day practice you have with each language (you say you code JS in the front-end now and then... I write a lot of it), but I still wanted to offer my counterargument.
List comprehensions are much better for this. Functional doesn't mean you have to use a function call. If you can use the paradigm with literal syntax, just do so.
Personally I would suggest Elixir (and the Phoenix web framework). It's fully 'parallel by default', will happily saturate all your cores and serve tens of thousands at the same time. The language is simple, the framework is good, although obviously not as many people know Elixir as Python.
I for instance know a bit of F#/C# and Java, but would probably pick Python to make a new product just to remove that mental barrier of not having my lack of language knowledge in the way of things
https://github.com/just-js/just
It seems to be a much tinier JavaScript runtime than Node.js (still using v8), but linux only
The benchmark is probably unrealistically optimized code but even so, it implies Node.js itself has a large performance overhead
If you pay attention you'll notice that just.js is using postgres as their DB, while all of the node benchmarks are handicapped by either using mongoose/mongodb or MySQL.
There is no node benchmark with postgres, but all of the fastest benchmarks used it.
"lithium" is a good example to show how much of an impact switching to postgres has. All of the 4 lithium benchmarks are identical except in what DB they use.
The results are: lithium-postgres-batch (#2 - 659850), lithium-postgres-beta (#13 - 398773), lithium-postgres (#14 - 398258), lithium (#45 - 271989). The last result is MySQL.
> [Multiple Queries] This is the first test where Just(js) has quite a big lead. This is likely due to the fact it is using a custom postgres client written in Javascript and taking full advantage of pipelining of requests. It also avoids sending a Sync/Commit on every query. As far as I am aware this is within the rules but will be happy to make changes to sync on every query if it is not.
https://just.billywhizz.io/blog/on-javascript-performance-01...
Further, for every framework/language that was tested with both MySQL and postgres (there's quite a few of them), the postgres one always ranks higher.
This is the "workload".
Perhaps it's because pg had better async drivers?
Long story short, fastapi was much much faster than anything else for us. It also felt a bit like flask. The integration with pydantic for validating dataclasses on the fly was also great.
1. how your solution is designed (your point, and I agree this is often done poorly)
2. the problem / work-load you are trying to scale (my point).
It might be that your problem does not "shard" very easily. You cannot fix this with solution architecture, at least not easily. Horizontal scaling of a relational database is very difficult, for example.Edit:
Another example. Can you rewrite NGINX in a slower language and use horizontal scaling to fix it? Of course not, because that horizontal scaling would itself leverage NGINX (or something like it)!
I am tempted to refactor the worker to use async features, and that would require factoring out uWSGI, which is fine, I only added it last week. The article states that Vibora is a drop in replacement for flask, but I guess I'm a bit skeptical, as I can't find much information outside of Vibora having a similar api. For a web service with basically one endpoint, I could refactor to another implementation fairly easily, I'm just looking for the right direction.
I thought maybe I should refactor the arch to either batch requests to the worker, or to use async. Anyone have a feeling where I should go? I am just getting started researching this, but any advice would be appreciated.
Edit: at least quart has a migration page.. probably will just try it out, what can I lose? https://pgjones.gitlab.io/quart/how_to_guides/flask_migratio...
Second edit: Also might try out polyrand's stack in the comments.
How does it compare to Sanic?
Then keep using Falcon! OP’s core thesis is that developer comfort matters more when choosing an API framework because the performance bottleneck is usually found elsewhere. The API itself should be fast enough with some combination of Gunicorn, gevent, PyPy, and horizontal scaling.
FastAPI [1] is also worth considering if you’re looking into asynchronous API frameworks. It comes with nice features for specifying API schemas.
https://www.techempower.com/benchmarks/#section=test&runid=8...
7.000.000 requests per second
Even GO can only achieve 4.500.000 million requests per secnod being a low-level language, in opposite to high-level C#.
[1] https://www.techempower.com/benchmarks/#section=data-r20&hw=...
Indeed. The problem is that many of the scores in the top 100 are really misleading because no one building a web app would implement things that way. There is still some value in the lower down benchmarks but you have to basically read the underlying source to determine if the implementation is remotely realistic or not. For starters I would ignore anything classified as "platform" which is described as:
Platform, meaning a raw server (not actually a framework at all). Good luck! You're going to need it.
For C# in particular I would only consider the mvc variants as realistic.Edit: I looked into the "asp.net core" composite score a bit more, it looks like those benchmarks are based on the aforementioned "platform" implementations for each test. I actually think this score is even more misleading than the individual benchmarks. At least the individual benchmarks show you the difference between "aspcore" (platform), "aspcore-mw" (middleware-only), and "aspcore-mvc" (full framework with routing).
Here are recalculated composite scores based on the more realistic implementations (aspcore-mvc, aspcore-mvc-ado-pg, aspcore-mvc-dap-pg, aspcore-mvc-ef-pg):
ASP.NET Core MVC with ADO.NET (raw SQL): 3029
ASP.NET Core MVC with Dapper: 2591
ASP.NET Core MVC with Entity Framework: 2195
Compared to Flask's 468 or Django's 280 it's still significantly faster, but not to the same extreme you might think at first glance at the chart.I am writing Django backends nowadays, but like I've mentioned elsewhere, I love its full-batteries approach and maturity, but I'm not very happy about it's meager async capabilities, and I think I'd prefer something with strong typing...
I've always been put off by Microsoft's lock-in but it seems that's changed so at least I'd put the .net as a contender for side project in the near future.
It's also a very "artificial" benchmark and real world code will give different results (if you have static content, just put it in a CDN and don't worry)
Other benchmarks from the same site:
- JSON Serialization: C# is number 34
- Single query: C# is number 23
- Fortunes: C# is number 7
https://github.com/TechEmpower/FrameworkBenchmarks/blob/mast...
There's no routing or templating, it just writes a bunch of strings. No one would build an actual web app this way.
The only C# benchmarks that are remotely realistic are the mvc variants, starting with aspcore-mvc-ado-pg at number 79.
Routing is required, but generally the rules allow things to be "reasonable" and "acceptable", which lets all these weird implementations through.
Honestly, they should remove the "Implementation approach" column, because basically every implementation is marked as "realistic", making it meaningless.
https://github.com/TechEmpower/FrameworkBenchmarks/blob/5b0e...
I mean I guess technically that's routing but it's not remotely realistic. Only the mvc variants use the actual framework's routing system.
Java and Rust are both above, but my takeaway is that it seems to be by far the best-performing batteries-included framework according to the benchmark. It leaves the likes of Rails, Laravel, Django, Phoenix, Spring or Nest.js in the dust.
Is there something in the benchmark that favors .net core above all the others?
I'm genuinely asking, I've never even tried .net but I like full-batteries frameworks and this catches my attention.
[1] https://www.techempower.com/benchmarks/#section=data-r20&hw=...
It's more interesting to see results of high-load DB tests, for example:
https://www.techempower.com/benchmarks/#section=data-r20&hw=...
A fast "Hello World" benchmark implies that the HTTP/transport layers of the framework are very fast. That's your base and the lower bound to your best performance potential. As a real world example if .NET ASP NET Core has the best request/response benchmark and its better than say nginx (a popular reverse proxy) it might be better to have all your gateways using a reverse proxy implemented with that as its base instead. Over your whole network depending on your scale that could be a big cost and latency saving measure. I wouldn't be surprised if Microsoft or it's community have started writing one.
( all those benchmarks are sort of useless anyway )
Also, high performance C# is so low level that you might as well write C++. Or you think you will use EF, LINQ and have 7 millions rps?
I'm imagining not, but it's still comparatively faster than most, if not all, "full-batteries frameworks", right?
I'm assuming the use of ORMs and such is more or less uniform in the comparison (eg, if they don't use EF for .net, they don't use Django ORM either)
The overall performance of .net across these benchmarks really catches my attention like it does GP...
I'm looking for a full-batteries framework based on a strongly typed language and never in my life I thought I'd say this, but it might be time to give .net / C# a whirl?
I've heard really good things about the dev experience from people here on HN, F# is a really cool bonus, and the fact that it looks at least comparatively performant could be the icing on the cake.
Getting an API hit from 300ms to 70ms, and proper frontend caching is far more valuable than concurrency (if you can afford to throw servers at it) because it actually affects user performance.
Using (for example) AWS Cloudfront was a gamechanger in how I design webapps and view performance. Being able to 'slice and dice' what requests get SSL terminated at the CDN, cached fairly locally, served from an Amazon managed webserver, or sent to our app server, increased our performance 10 fold.
That approach isn't always practical, but I find that it's now much easier to choose the backend for developer performance and doubling the server CPU/memory is quicker and cheaper when needed.
https://lucumr.pocoo.org/2010/4/3/april-1st-post-mortem/
Flask author reflects on that here:
http://mitsuhiko.pocoo.org/flask-pycon-2011.pdf
Quite relevant to the conclusion in the article.
In general, I've always liked the idea of pypy, so I'll try to use it more, and not just for performance. Will also donate when I can.
Not quite sure if their current site's code is opensource... anyone know?
Of course Reddit/FB/EVE/Dropbox shards everything, there's no global state to manage via Python. The state lives in the data store layer.
And for that there are these monstrous/elegant things like Vitess, that YouTube used (uses?): https://vitess.io/docs/overview/architecture/ which is basically a sharding/routing layer on top of independent MySQL instances.
Please donate. Pypy needs funds - https://opencollective.com/pypy
Pypy doesnt get a fraction of the funding that python does.
You got loads of libraries built around flask, and a lot of doc and tutorials, not to mention how much battle tested it is.
Those can matter much more than hypothetical perfs in a synthetic benchmark.
Ok what's the secret sauce and downsides? Because as far as I know there's no way to cheat with PHP like C# did here:
https://github.com/TechEmpower/FrameworkBenchmarks/blob/mast...
https://www.techempower.com/benchmarks/#section=data-r20&hw=...
> all of this emphasises the fact that unless you have some super-niche use-case in mind, it's actually a better idea to choose your framework based upon ergonomics and features, rather than speed.
What matters is that the cumulative latency & throughput solve your problem, not how fast you can make one singular async worker thread.
I figure most people running complex web services in production would just do an eye roll at this post. Nobody's going to switch to PyPy for any of this.
My team at work runs several complex ML workloads, and we use the exact same container pattern for every service running gunicorn to spawn X async workers per pod and then scale pods per service to meet throughput requirements. Sometimes we also just post complex image processing workloads to a queue and batch them to GPU processor workers. In all these use cases, super low effort “just toss it in gunicorn running flask” has worked without issue for services supporting up to peak load of thousands to hundreds of thousands of requests per second.
Without loading the image, the text is the same color.
This is stateless HTTP requests hitting a relational database. How is this dead horse still being beaten? The patterns for load balancing, horizontal scalability, caching in this space well documented.
What are we gaining still profiling Django, Flask and Ruby on Rails in 2021.