220

I want to do parallel http request tasks in asyncio, but I find that python-requests would block the event loop of asyncio. I've found aiohttp but it couldn't provide the service of http request using a http proxy.

So I want to know if there's a way to do asynchronous http requests with the help of asyncio.

3
  • 1
    If you are just sending requests you could use subprocess to parallel your your code. Commented Mar 5, 2014 at 6:43
  • 1
    There is now an asyncio port of requests. github.com/rdbhost/yieldfromRequests Commented Mar 23, 2015 at 15:21
  • 5
    This question is also useful for cases where something indirectly relies on requests (like google-auth) and can't be trivially rewritten to use aiohttp. Commented Apr 17, 2021 at 12:12

9 Answers 9

247

To use requests (or any other blocking libraries) with asyncio, you can use BaseEventLoop.run_in_executor to run a function in another thread and yield from it to get the result. For example:

import asyncio
import requests

@asyncio.coroutine
def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://www.google.com')
    future2 = loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    response1 = yield from future1
    response2 = yield from future2
    print(response1.text)
    print(response2.text)

asyncio.run(main())

This will get both responses in parallel.

With python 3.5 you can use the new await/async syntax:

import asyncio
import requests

async def main():
    loop = asyncio.get_event_loop()
    response1 = await loop.run_in_executor(None, requests.get, 'http://www.google.com')
    response2 = await loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    print(response1.text)
    print(response2.text)

asyncio.run(main())

See PEP0492 for more.

Sign up to request clarification or add additional context in comments.

19 Comments

Can you explain how exactly this works? I don't understand how this doesn't block.
@christian but if its running concurrently in another thread, isn't that defeating the point of asyncio?
@christian Yeah, the part about it firing a call off and resuming execution makes sense. But if I understand correctly, requests.get will be executing in another thread. I believe one of the big pros of asyncio is the idea of keeping things single-threaded: not having to deal with shared memory, locking, etc. I think my confusion lies in the fact that your example uses both asyncio and concurrent.futures module.
@scoarescoare That's where the 'if you do it right' part comes in - the method you run in the executor should be self-contained ((mostly) like requests.get in the above example). That way you don't have to deal with shared memory, locking, etc., and the complex parts of your program are still single threaded thanks to asyncio.
Really cool that this works and so is so easy for legacy stuff, but should be emphasised this uses an OS threadpool and so doesn't scale up as a true asyncio oriented lib like aiohttp does
|
115

aiohttp can be used with HTTP proxy already:

import asyncio
import aiohttp


async def do_request():
    proxy_url = 'http://localhost:8118'  # your proxy address
    response = await aiohttp.request(
        'GET', 'http://google.com',
        proxy=proxy_url,
    )
    return response

loop = asyncio.get_event_loop()
loop.run_until_complete(do_request())

4 Comments

What does the connector do here?
It provides a connection through proxy server
This is a much better solution then to use requests in a separate thread. Since it is truly async it has lower overhead and lower mem usage.
for python >=3.5 replace @asyncio.coroutine with "async" and "yield from" with "await"
95

The answers above are still using the old Python 3.4 style coroutines. Here is what you would write if you got Python 3.5+.

aiohttp supports http proxy now

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
            'http://python.org',
            'https://google.com',
            'http://yifei.me'
        ]
    tasks = []
    async with aiohttp.ClientSession() as session:
        for url in urls:
            tasks.append(fetch(session, url))
        htmls = await asyncio.gather(*tasks)
        for html in htmls:
            print(html[:100])

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

There is also the httpx library, which is a drop-in replacement for requests with async/await support. However, httpx is somewhat slower than aiohttp.

Another option is curl_cffi, which has the ability to impersonate browsers' ja3 and http2 fingerprints.

4 Comments

could you elaborate with more urls? It does not make sense to have only one url when the question is about parallel http request.
@ospider How this code can be modified to deliver say 10k URLs using 100 requests in parallel? The idea is to use all 100 slots simultaneously, not to wait for 100 to be delivered in order to start next 100.
@AntoanMilkov That's a different question that can not be answered in the comment area.
@ospider You are right, here is the question: stackoverflow.com/questions/56523043/…
14

Requests does not currently support asyncio and there are no plans to provide such support. It's likely that you could implement a custom "Transport Adapter" (as discussed here) that knows how to use asyncio.

If I find myself with some time it's something I might actually look into, but I can't promise anything.

2 Comments

The link leads to a 404.
@Lukasa Any attempts on this?
12

There is a good case of async/await loops and threading in an article by Pimin Konstantin Kefaloukos Easy parallel HTTP requests with Python and asyncio:

To minimize the total completion time, we could increase the size of the thread pool to match the number of requests we have to make. Luckily, this is easy to do as we will see next. The code listing below is an example of how to make twenty asynchronous HTTP requests with a thread pool of twenty worker threads:

# Example 3: asynchronous requests with larger thread pool
import asyncio
import concurrent.futures
import requests

async def main():

    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:

        loop = asyncio.get_event_loop()
        futures = [
            loop.run_in_executor(
                executor, 
                requests.get, 
                'http://example.org/'
            )
            for i in range(20)
        ]
        for response in await asyncio.gather(*futures):
            pass


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

5 Comments

problem with this is that if I need to run 10000 requests with chunks of 20 executors, I have to wait for all 20 executors to finish in order to start with the next 20, right? I cannot do for for i in range(10000) because one requests might fail or timeout, right?
Can you pls explain why do you need asyncio when you can do the same just using ThreadPoolExecutor?
@lya Rusin Based on what, do we set the number of max_workers? Does it have to do with number of CPUs and threads?
@AsafPinhassi if the rest of your script/program/service is asyncio, you'll want to use it "all the way". you'd probably be better off using aiohttp (or some other lib that supports asyncio)
@alt-f4 it actually does not matter how many CPU you have. The point of delegating this work to a thread (and the whole point of asyncio) is for IO bound operations. The thread will simply by idle ("waiting") for the response to retrieved from the socket. asyncio enables to actually handle many concurrent (not parallel!) requests with no threads at all (well, just one). However, requests does not support asyncio so you need to create threads to get concurrency.
10

Considering that aiohttp is fully featured web framework, I’d suggest to use something more light weighted like httpx (https://www.python-httpx.org/) which supports async requests. It has almost identical api to requests:

>>> async with httpx.AsyncClient() as client:
...     r = await client.get('https://www.example.com/')
...
>>> r
<Response [200 OK]>

1 Comment

There is a nice article covering this topic blog.jonlu.ca/posts/async-python-http
3

python-requests does not natively support asyncio yet. Going with a library that natively supports asyncio, like httpx would be the most beneficial approach.

However, if your use cases heavily relies on using python-requests you can wrap the sync calls with asyncio.to_thread and asyncio.gather and follow the asyncio programming patterns.

import asyncio
import requests

async def main():
    res = await asyncio.gather(asyncio.to_thread(requests.get("YOUR_URL"),)

if __name__ == "__main__":
    asyncio.run(main())

For concurrency/parallelization of the network requests:

import asyncio
import requests

urls = ["URL_1", "URL_2"]

async def make_request(url: string):
    response = await asyncio.gather(asyncio.to_thread(requests.get(url),)
    return response

async def main():
    responses = await asyncio.gather((make_request(url) for url in urls))
    for response in responses:
        print(response)

if __name__ == "__main__":
    asyncio.run(main())

Comments

2

DISCLAMER: Following code creates different threads for each function.

This might be useful for some of the cases as it is simpler to use. But know that it is not async but gives illusion of async using multiple threads, even though decorator suggests that.

To make any function non blocking, simply copy the decorator and decorate any function with a callback function as parameter. The callback function will receive the data returned from the function.

import asyncio
import requests


def run_async(callback):
    def inner(func):
        def wrapper(*args, **kwargs):
            def __exec():
                out = func(*args, **kwargs)
                callback(out)
                return out

            return asyncio.get_event_loop().run_in_executor(None, __exec)

        return wrapper

    return inner


def _callback(*args):
    print(args)


# Must provide a callback function, callback func will be executed after the func completes execution !!
@run_async(_callback)
def get(url):
    return requests.get(url)


get("https://google.com")
print("Non blocking code ran !!")

Comments

1

Requests does not support asyncio. You can use aiohttp instead, since aiohttp fully supports asyncio and has better performance than requests.

Alternatively, you can use requests with traditional multithreading:

import concurrent.futures
import requests

def main():
    with concurrent.futures.ThreadPoolExecutor() as executor:
        feature1 = executor.submit(requests.get, 'http://www.google.com')
        feature2 = executor.submit(requests.get, 'http://www.google.co.uk')
        print(feature1.result().text)
        print(feature2.result().text)

main()

You can use loop.run_in_executor to integrate executor into asyncio. The above code is semantically equivalent to:

import asyncio
import requests

@asyncio.coroutine
def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://www.google.com')
    future2 = loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    response1 = yield from future1
    response2 = yield from future2
    print(response1.text)
    print(response2.text)

asyncio.run(main())

With this approach, you can use any other blocking library with asyncio.

With Python 3.5+ you can use the new await/async syntax:

import asyncio
import requests

async def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://www.google.com')
    future2 = loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    print((await future1).text)
    print((await future2).text)

asyncio.run(main())

See PEP 492 for more.

With Python 3.9+, it's even simpler using asyncio.to_thread:

import asyncio
import requests

async def main():
    future1 = asyncio.to_thread(requests.get, 'http://www.google.com')
    future2 = asyncio.to_thread(requests.get, 'http://www.google.co.uk')
    print((await future1).text)
    print((await future2).text)

asyncio.run(main())

asyncio.to_thread has another advantage: asyncio.to_thread accepts keyword arguments, while loop.run_in_executor doesn't.

Keep in mind that all of the above code actually uses multithreading under the hood instead of asyncio, so consider using an asynchronous HTTP client such as aiohttp to achieve true asynchrony.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.