(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

How to Make Parallel API Calls in Python with asyncio

João Barros 07 de July de 2026 4 min read

When a program needs to call an API dozens or hundreds of times — for example, to feed a dashboard or gather data from several endpoints — doing it one call at a time becomes very slow. Parallel API calls in Python solve this problem: instead of waiting for each request to finish before starting the next, we send several at once and the total time drops dramatically.

Prerequisites

  • Python 3.8 or later installed.
  • Knowing how to make a simple request with requests or httpx.
  • A terminal to install packages with pip.
  • A willingness to experiment: we will use the public testing API jsonplaceholder.typicode.com.

Step 1: Understand why sequential calls are slow

Imagine you need to fetch 20 tasks from an API. With a normal loop, each request only starts when the previous one finishes. If each takes about 0.2 seconds, the total is around 4 seconds — and it grows as you add more requests.

import time, httpx

inicio = time.perf_counter()
with httpx.Client() as cliente:
    for i in range(1, 21):
        r = cliente.get(f"https://jsonplaceholder.typicode.com/todos/{i}")
        r.json()
print(f"Sequencial: {time.perf_counter() - inicio:.2f} s")

The problem is not that your computer is busy: it is almost always waiting for the network response. That idle time, when nothing is happening, is exactly what we will use to get other requests moving.

Step 2: Install httpx

httpx is a modern HTTP client that works synchronously (like requests) and also asynchronously, which is ideal for parallel requests. Install it with pip:

pip install httpx

Step 3: Create an async function for a single call

In asynchronous code we use async def to define the function and await to wait for the response without blocking the rest of the program. While one request waits for the network, Python is free to handle the others. This function fetches one task and returns the JSON.

async def obter(cliente, id):
    r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
    return r.json()

Notice that the function receives the cliente as an argument: this way every call reuses the same connection, which is faster than opening a new one each time.

Step 4: Run several calls in parallel with asyncio.gather

asyncio.gather takes several tasks and runs them at the same time, returning the results in the same order you requested them. It is the heart of parallel calls.

import asyncio, time, httpx

async def obter(cliente, id):
    r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
    return r.json()

async def principal():
    inicio = time.perf_counter()
    async with httpx.AsyncClient() as cliente:
        tarefas = [obter(cliente, i) for i in range(1, 21)]
        resultados = await asyncio.gather(*tarefas)
    print(f"Paralelo: {time.perf_counter() - inicio:.2f} s")
    return resultados

resultados = asyncio.run(principal())

The *tarefas unpacks the list into individual arguments. When you run it, the total time usually goes from several seconds to a few tenths of a second.

Step 5: Limit concurrency with a Semaphore

Sending 500 requests at once can overload the API and trigger the 429 Too Many Requests error. A Semaphore limits how many calls happen at the same time, so you stay a good neighbor and avoid being blocked.

import asyncio, httpx

async def obter(cliente, limite, id):
    async with limite:
        r = await cliente.get(f"https://jsonplaceholder.typicode.com/todos/{id}")
        return r.json()

async def principal():
    limite = asyncio.Semaphore(5)  # no máximo 5 ao mesmo tempo
    async with httpx.AsyncClient(timeout=10) as cliente:
        tarefas = [obter(cliente, limite, i) for i in range(1, 21)]
        return await asyncio.gather(*tarefas)

resultados = asyncio.run(principal())
print(f"Recebidas {len(resultados)} respostas")

With Semaphore(5), only five requests run at the same time; the rest automatically wait their turn.

Check the result

To confirm everything worked, compare the time printed by the sequential version (Step 1) with the parallel one (Step 4): it should be much lower. Also check that len(resultados) equals the number of requests you made (20) and that no result is empty. If network errors appear, increase the timeout or lower the Semaphore value.

Conclusion

You have learned to turn slow, one-by-one calls into parallel requests that make use of the network waiting time, and to control the load with a Semaphore. The next step is to add error handling — for example, retrying failed requests — and to collect results as they arrive with asyncio.as_completed. Have you thought about how many requests per second your API can handle before it returns 429?