New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTTPX AsyncClient slower than aiohttp? #838
Comments
Thanks, this is very interesting quantified insights! I went ahead and added the aiohttp/httpx ratio to your table. I used your script to run the benchmark on my own machine, and observe similar results:
|
The The What's more surprising to me is, as you said, the 3x higher I'll run httpxprof over your scripts and see what other insights I can come up with. :-) |
Okay, so apparently one massively useless thing we do is eager TLS setup on If we run:
We get this view: => 92% (!) of the time spent instantiating the client (which itself is 62% of the total time making a single request with a client) is spent setting up SSL ( Compare this to I used the following to get the profile above: # aiohttp_single.py
import asyncio
import aiohttp
import httpxprof
async def main() -> None:
for _ in httpxprof.requests():
async with aiohttp.ClientSession() as session:
async with session.get(httpxprof.url):
pass
asyncio.run(main()) httpxprof run aiohttp_single.py
httpxprof view aiohttp_single.py So one action point already might be to lazily load the SSL configuration on the first request that uses HTTPS. |
I might be missing something, but what if loading the SSL context is needed in both aiohttp and httpx, is the time doing so somewhat similar? And the total time? I can also try to take this numbers later I see that in the gist the host is http and not https, that’s why I’m questioning this |
Yes, I actually just realized that optimizing for requests against an insecure host is probably marginally useful for real-world situations anyway. We should be comparing aiohttp and HTTPX requesting a host over HTTPS. I'm going to setup a local server with HTTPS turned on (see instructions here and run the profiling again, this time requesting |
Don't know how much this would affect the final result, but |
Yep, an important one is a bunch of microservices spinning on localhost |
@imbolc I forked your gist and created a version that runs Running it on my machine, I get the updated results below:
The difference for the single request case went from 8x to 2-3x, which is more reasonable, and not entirely surprising to me (we haven't been very focused in optimization so far). Besides, running
(The improvement with the previous setup might come from the fact that I now explicitly pass The So actually, there's no real burden on our side due to TLS/SSL. Still, |
Ah, so from
I tried using |
Don't know if it 100% performance related, but on #832 I added a repository with sample code where timeouts happen with |
Hi, going to close this off as "yes, we're maybe 2x-3x slower than aiohttp, but getting up to speed there isn't a priority for 1.0", though PRs on anything that might be a performance burden are still very much welcome! Thanks all. |
I'd be pretty skeptical that we're comparing like-for-like (eg. there's various SSL, If we do look at any benchmarking at some point, I'd want to look at measurements after a client instance is created, since you really want to be instantiating a single client instance which is then used for the lifetime of the app. |
It's included into the benchmark, the second row in the table |
How do you got the code Flame Graph ? It look like usefull |
@JustJia It's from |
Hello @tomchristie ; Would like to ask your current thoughts about the speed comparison between aiohttp and httpx, since some time has passed after this comment. I believe these two sources contain a fair comparison between the two libraries. It seems aiohttp is relatively faster than httpx, I wonder what could be the reasons for that?
Let me reference tom's comment about the advantages of httpx . |
I've not dug into the places you've linked, but a couple of things to note...
For certain types of requests that'll make a difference. Also we fairly recently resolved an issue with upload speeds (#1948) Tho I'll happily spend time looking into this if what you've posted doesn't match up with either of those two possible cases. |
@tomchristie that certainly could be the case, since all the requests are sent to the same ip in both of the benchmarks. Supporting DNS caching would be great for httpx/httpcore as well. |
As I can see, we try to load ca certificates for every connection (single request case). class SSLConfig:
...
DEFAULT_CA_BUNDLE_PATH = Path(certifi.where())
...
def load_ssl_context_verify(self) -> ssl.SSLContext:
elif isinstance(self.verify, bool):
ca_bundle_path = self.DEFAULT_CA_BUNDLE_PATH
...
if ca_bundle_path.is_file():
logger.trace(f"load_verify_locations cafile={ca_bundle_path!s}")
context.load_verify_locations(cafile=str(ca_bundle_path))
elif ca_bundle_path.is_dir():
logger.trace(f"load_verify_locations capath={ca_bundle_path!s}")
context.load_verify_locations(capath=str(ca_bundle_path))
...
As @florimondmanca says, the custom context without extra verification removes the huge gap from aiohttp. Should we do it the same way? Or what is the reason to do it our way? |
Just want to chime in here and say that I've been noticing load_ssl_context_verify to be a huge performance problem in my app. We make a lot our network calls and most of our time is spent in load_ssl_context_verify |
did some benchmarking on this and caching the ssl context makes a huge difference in perf. ugly hack for testing, but the general idea for the caching: diff --git a/httpx/_config.py b/httpx/_config.py
index d164e4c..262763b 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -50,6 +50,7 @@ def create_ssl_context(
cert=cert, verify=verify, trust_env=trust_env, http2=http2
).ssl_context
+context = None
class SSLConfig:
"""
@@ -99,11 +100,17 @@ class SSLConfig:
"""
Return an SSL context for verified connections.
"""
+ global context
if self.trust_env and self.verify is True:
ca_bundle = get_ca_bundle_from_env()
if ca_bundle is not None:
self.verify = ca_bundle
+ if context is None:
+ context = ssl.create_default_context()
+ self._load_client_certs(context)
+ return context
+
if isinstance(self.verify, ssl.SSLContext):
# Allow passing in our own SSLContext object that's pre-configured.
context = self.verify some flame graphs generated with pyspy:
|
The work around mentioned in #838 (comment) can also be used on client instantiation: import asyncio
import httpx
import ssl
URL = "https://example.com"
# "cache" at module level
context = ssl.create_default_context()
async def main() -> None:
while True:
async with httpx.AsyncClient(verify=context) as client:
r = await client.get(URL)
print(r.status_code)
if __name__ == '__main__':
asyncio.run(main()) Running the following code w/o and w/ the import asyncio
import httpx
import ssl
# "cache" at module level
context = ssl.create_default_context()
async def main() -> None:
while True:
async with httpx.AsyncClient(verify=context) as client:
print("foo")
if __name__ == '__main__':
asyncio.run(main()) |
Work around for a perf issue with how httpx handles SSL. Instead of using `httpx.AsyncClient` directly, we subclass and reuse the ssl context. rel: encode/httpx#838
* Create HTTPX_SSL_CONTEXT once and use it in AsyncClient instances Background: encode/httpx#838 * Refactor OIDCUser.__call_ to exit early and create a scoped AsyncClient with existing ssl context * Simplify refresh_client_creds_token * Bump version to 1.3.4
I just found async-client to be significantly slower comparing to aiohttp, escecially for single request:
I tried both release and master, results are pretty the same. The code of the benchmark is here: https://gist.github.com/imbolc/15cab07811c32e7d50cc12f380f7f62f
The text was updated successfully, but these errors were encountered: