Ga naar hoofdinhoud

Asynchroon programmeren in Python: de complete gids

Versnel je code met asynchroon programmeren in Python. Een stapsgewijze gids voor asyncio, concurrency, efficiënte HTTP-verzoeken en database-integratie.
Bijgewerkt 2 jun 2026  · 14 min lezen

Terwijl je Python-script geduldig wacht tot API-responses, databasequeries of bestandsbewerkingen zijn afgerond, blijft die tijd vaak onbenut. Met asynchroon programmeren in Python kan je code meerdere taken tegelijk afhandelen. Dus terwijl de ene operatie wacht, gaan andere door, waardoor lege momenten productief worden en wachttijden van minuten vaak teruglopen naar seconden.

In deze gids leer ik je de basis van async programmeren in Python aan de hand van mini-projecten. Je ziet hoe coroutines, event loops en async I/O je code veel responsiever kunnen maken.

Wil je leren hoe je asynchrone web-API's bouwt? Bekijk dan zeker deze cursus over FastAPI.

Wat is asynchroon programmeren in Python?

In traditioneel synchroon Python voert je code één regel tegelijk uit. Als je bijvoorbeeld een API aanroept, stopt je programma en wacht op het antwoord. Duurt dat twee seconden, dan staat je hele programma twee seconden stil. Asynchroon programmeren laat je code een API-aanroep starten en vervolgens doorgaan met andere taken.

Wanneer de response binnenkomt, pakt je code de draad weer op. In plaats van te wachten tot elke operatie klaar is, kun je meerdere operaties tegelijk uitvoeren. Dit is vooral van belang wanneer je code wacht op externe systemen zoals databases, API's of bestandssystemen om te reageren. 

Om dit te laten werken, gebruikt het async-systeem van Python een paar kernconcepten:

  • Coroutines: functies gedefinieerd met async def in plaats van def. Ze kunnen de uitvoering pauzeren en hervatten, waardoor ze ideaal zijn voor operaties waarbij je moet wachten.

  • await: Dit trefwoord zegt tegen Python: "pauzeer deze coroutine tot deze operatie klaar is, maar laat in de tussentijd andere code draaien."

  • Event loop: De motor die al je coroutines beheert en beslist welke wanneer draait en wanneer ertussen wordt gewisseld.

  • Taken (tasks): Coroutines verpakt voor gelijktijdige uitvoering. Je maakt ze met asyncio.create_task() om meerdere operaties tegelijk te laten draaien.

Om verwarring te voorkomen over wat async programmeren wel (en niet) kan, houd dit in gedachten:

  • Async werkt het best met I/O-gebonden werk zoals HTTP-verzoeken, databasequeries en bestandsoperaties, waarbij je code wacht op externe systemen.

  • Async helpt niet bij CPU-gebonden werk zoals complexe berekeningen of dataverwerking, waarbij je code actief rekent in plaats van wacht.

De beste manier om deze concepten eigen te maken, is door echte async code te schrijven. In de volgende sectie maak je je eerste async functie en zie je precies hoe coroutines en de event loop samenwerken.

Je eerste asynchrone Python-functie

Voordat we async code schrijven, kijken we naar een reguliere synchrone functie die wacht voordat hij iets doet:

import time

def greet_after_delay():
    print("Starting...")
    time.sleep(2)  # Blocks for 2 seconds
    print("Hello!")

greet_after_delay()
Starting...
Hello!

De functie werkt, maar time.sleep(2) blokkeert je hele programma. Er kan tijdens die twee seconden niets anders draaien.

Hier is de async-versie:

import asyncio

async def greet_after_delay():
    print("Starting...")
    await asyncio.sleep(2)  # Pauses, but doesn't block
    print("Hello!")

asyncio.run(greet_after_delay())
Starting...
Hello!

De uitvoer lijkt identiek, maar onder de motorkap gebeurt iets anders. Drie veranderingen maken dit async:

  1. async def in plaats van def declareert dit als een coroutine.

  2. await asyncio.sleep(2) in plaats van time.sleep(2) pauzeert zonder te blokkeren.

  3. asyncio.run() start de event loop en voert de coroutine uit.

Let op: asyncio.sleep() is zelf een async functie, daarom heeft het await nodig. Dit is een belangrijke regel: elke async functie moet worden aangeroepen met await. Of het nu een ingebouwde functie zoals asyncio.sleep() is of eentje die je zelf schrijft: als je await vergeet, wordt hij niet echt uitgevoerd.

Op dit moment lijkt de async-versie niet sneller. Dat komt omdat we maar één taak hebben. Het echte voordeel zie je wanneer je meerdere coroutines tegelijk draait, wat we in de volgende sectie behandelen.

Nog iets belangrijks: je kunt een async functie niet rechtstreeks aanroepen zoals een gewone functie. Laten we het proberen:

result = greet_after_delay()
print(result)
print(type(result))
<coroutine object greet_after_delay at 0x...>
<class 'coroutine'>

Het aanroepen van greet_after_delay() geeft een coroutine-object terug, niet het resultaat. De functie draait niet echt. Je hebt asyncio.run() of await nodig om hem binnen een andere functie uit te voeren.

Hoe de event loop werkt

De event loop is de motor achter async programmeren. Hij beheert je coroutines en beslist wat wanneer draait. Dit gebeurt stap voor stap wanneer je de async-functie greet_after_delay() uitvoert:

  1. asyncio.run() maakt een event loop.

  2. De event loop start greet_after_delay().

  3. "Starting..." wordt geprint.

  4. Komt bij await asyncio.sleep(2) → coroutine pauzeert.

  5. Event loop checkt: "Zijn er andere taken om te draaien?" (nu nog niet).

  6. 2 seconden gaan voorbij, sleep is klaar.

  7. Event loop hervat greet_after_delay().

  8. "Hello!" wordt geprint.

  9. Functie eindigt → event loop stopt.

Event loop-engine in async programmeren in Python uitgelegd

Stap 5 is waar async interessant wordt. Met één coroutine is er niets anders te doen. Maar als je meerdere coroutines hebt, schakelt de event loop over naar ander werk terwijl er één wacht. In plaats van stil te zitten tijdens een sleep van twee seconden, kan hij andere code uitvoeren.

Zie de event loop als een verkeersleider. Hij maakt individuele auto's niet sneller. Hij houdt het verkeer in beweging door andere auto's te laten gaan terwijl er één stilstaat.

Veelgemaakte async-fout: await vergeten

Een veelgemaakte beginnersfout is await vergeten bij het aanroepen van een coroutine binnen een andere async functie:

import asyncio

async def get_message():
    await asyncio.sleep(1)
    return "Hello!"

async def main():
    message = get_message()  # Missing await!
    print(message)

asyncio.run(main())
<coroutine object get_message at 0x...>
RuntimeWarning: coroutine 'get_message' was never awaited

Zonder await krijg je het coroutine-object in plaats van de returnwaarde. Python waarschuwt ook dat de coroutine nooit is uitgevoerd.

De oplossing is simpel:

async def main():
    message = await get_message()  # Added await
    print(message)

asyncio.run(main())
Hello!

Zie je een RuntimeWarning over een niet-geawaite coroutine, controleer dan of je await hebt gebruikt bij elke async aanroep.

Gelijktijdige asynchrone Python-taken 

In de vorige sectie hebben we een sync functie naar async omgezet. Maar het was niet sneller. Dat komt doordat we slechts één coroutine draaiden. De echte kracht van async zie je wanneer je meerdere coroutines tegelijk laat lopen.

Waarom sequentieel await nog steeds sequentieel is

Je zou denken dat het aanroepen van meerdere async functies ze automatisch gelijktijdig laat draaien. Maar kijk wat er gebeurt als we greet_after_delay() drie keer aanroepen:

import asyncio
import time

async def greet_after_delay(name):
    print(f"Starting {name}...")
    await asyncio.sleep(2)
    print(f"Hello, {name}!")

async def main():
    start = time.perf_counter()
    
    await greet_after_delay("Alice")
    await greet_after_delay("Bob")
    await greet_after_delay("Charlie")
    
    elapsed = time.perf_counter() - start
    print(f"Total time: {elapsed:.2f} seconds")

asyncio.run(main())
Starting Alice...
Hello, Alice!
Starting Bob...
Hello, Bob!
Starting Charlie...
Hello, Charlie!
Total time: 6.01 seconds

Zes seconden voor drie taken van twee seconden. Elke await wacht tot zijn coroutine klaar is voordat de volgende regel doorgaat. De code is async, maar draait sequentieel.

Async gelijktijdig uitvoeren met asyncio.gather() 

Om coroutines tegelijk te laten draaien, gebruik je asyncio.gather(). Dat neemt meerdere coroutines en voert ze gelijktijdig uit:

async def main():
    start = time.perf_counter()
    
    await asyncio.gather(
        greet_after_delay("Alice"),
        greet_after_delay("Bob"),
        greet_after_delay("Charlie"),
    )
    
    elapsed = time.perf_counter() - start
    print(f"Total time: {elapsed:.2f} seconds")

asyncio.run(main())
Starting Alice...
Starting Bob...
Starting Charlie...
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Total time: 2.00 seconds

Twee seconden in plaats van zes. Alle drie de coroutines startten meteen, sliepen tegelijk en eindigden samen. Dat is een 3x versnelling met één wijziging.

Let op de volgorde van de uitvoer: alle drie de "Starting..."-berichten verschijnen voordat er "Hello..."-berichten komen. Dit laat zien dat alle coroutines tijdens hetzelfde venster van twee seconden draaien in plaats van op elkaar te wachten.

asyncio.gather() retourneert een lijst met resultaten in dezelfde volgorde als waarin je de coroutines hebt meegegeven. Als je coroutines waarden teruggeven, kun je die opvangen:

async def fetch_number(n):
    await asyncio.sleep(1)
    return n * 10

async def main():
    results = await asyncio.gather(
        fetch_number(1),
        fetch_number(2),
        fetch_number(3),
    )
    print(results)

asyncio.run(main())
[10, 20, 30]

De resultaten komen terug in de volgorde [10, 20, 30], overeenkomend met de volgorde van de coroutines die je aan gather() doorgaf.

Asynchrone HTTP-verzoeken in Python met aiohttp

Tot nu toe hebben we asyncio.sleep() gebruikt om vertragingen te simuleren. Nu gaan we echte HTTP-verzoeken doen. Je grijpt misschien naar de bibliotheek requests, maar die werkt hier niet. requests is synchroon en blokkeert de event loop, wat het doel van async tenietdoet.

Gebruik in plaats daarvan aiohttp, een async HTTP-client die hiervoor is gebouwd.

Introductie tot aiohttp

Zo haal je een URL op met aiohttp:

import aiohttp
import asyncio

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

async def main():
    html = await fetch("https://example.com")
    print(f"Fetched {len(html)} characters")

asyncio.run(main())
Fetched 513 characters

Let op de twee geneste async with-blokken. Elk beheert een andere resource, en begrijpen wat ze doen is de sleutel tot correct gebruik van aiohttp.

Hoe ClientSession werkt in aiohttp

Dit gebeurt stap voor stap wanneer je verzoeken doet met aiohttp:

  1. aiohttp.ClientSession() maakt een connectiepool (eerst leeg).

  2. session.get(url) checkt de pool: "Is er een open verbinding met deze host (de server van de website)?"

  3. Als er geen verbinding is, worden een nieuwe TCP-verbinding (het basisprotocol voor dataverkeer op internet) en een SSL-handshake (de encryptieopzet voor HTTPS) opgezet.

  4. Er wordt een HTTP-verzoek verzonden en we wachten op de responseheaders.

  5. Het response-object houdt de verbinding vast.

  6. await response.text() leest de bodydata van het netwerk.

  7. Exit van de binnenste async with-lus: de verbinding gaat terug naar de pool (blijft open!).

  8. Het volgende verzoek naar dezelfde host wordt gedaan en hergebruikt de verbinding uit de pool (stap 3 wordt overgeslagen).

  9. Exit van de buitenste async with-lus: alle verbindingen in de pool sluiten.

Stap 7 en 8 zijn de kerninzichten. De connectiepool houdt verbindingen tussen verzoeken in leven. Als je een nieuw verzoek naar dezelfde host doet, sla je de TCP- en SSL-handshake volledig over.

aiohttp-werkstroom voor connection pooling: een gedeelde sessie gebruiken voor meerdere verzoeken

Dit is belangrijk omdat het opzetten van een nieuwe verbinding traag is. Een TCP-handshake kost één round-trip naar de server. Een SSL-handshake kost er nog twee. Afhankelijk van de latentie is dat 100-300 ms voordat je zelfs maar je eerste byte data verstuurt.

Een gedeelde sessie voor alle verzoeken gebruiken

Nu zie je waarom voor elk verzoek een nieuwe sessie maken problematisch is:

# Wrong: new session for each request
async def fetch_bad(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    urls = ["https://example.com"] * 10
    results = await asyncio.gather(*[fetch_bad(url) for url in urls])

Elke aanroep van fetch_bad() maakt een nieuwe sessie met een lege pool. Elk verzoek betaalt de volledige handshake-kost, zelfs als ze allemaal naar dezelfde host gaan.

De oplossing is één sessie maken en die aan je fetch-functie doorgeven:

# Right: reuse a single session
async def fetch_good(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = ["https://example.com"] * 10
    async with aiohttp.ClientSession() as session:
        results = await asyncio.gather(*[fetch_good(session, url) for url in urls])

Met een gedeelde sessie zet het eerste verzoek de verbinding op en hergebruiken de overige negen die. Één handshake in plaats van tien.

Voorbeeld van async HTTP-verzoek: Hacker News scrapen

Laten we dit toepassen met de Hacker News API. Deze API is perfect om asynchroon gedrag te demonstreren omdat het ophalen van verhalen meerdere verzoeken vereist. Als je nieuw bent met het werken met REST API's in Python, bekijk dan Python API's: een gids voor het bouwen en gebruiken van API's voor de basisconcepten.

De structuur van de Hacker News API:

  • https://hacker-news.firebaseio.com/v0/topstories.json geeft een lijst met story-ID's (alleen nummers)

  • https://hacker-news.firebaseio.com/v0/item/{id}.json geeft details van één verhaal terug

Om 10 verhalen te krijgen, heb je 11 verzoeken nodig: één voor de ID-lijst en vervolgens één per verhaal. Precies daar blinkt async programmeren in uit.

Laten we eerst kijken wat de API teruggeeft als we proberen het eerste verhaal op te halen:

import aiohttp
import asyncio

HN_API = "https://hacker-news.firebaseio.com/v0"

async def main():
    async with aiohttp.ClientSession() as session:
        # Get top story IDs
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()
        
        print(f"Found {len(story_ids)} stories")
        print(f"First 5 IDs: {story_ids[:5]}")
        
        # Fetch first story details
        first_id = story_ids[0]
        async with session.get(f"{HN_API}/item/{first_id}.json") as response:
            story = await response.json()
        
        print(f"\nStory structure:")
        for key, value in story.items():
            print(f"  {key}: {repr(value)[:50]}")

asyncio.run(main())
Found 500 stories
First 5 IDs: [46051449, 46055298, 46021577, 46053566, 45984864]

Story structure:
  by: 'mikeayles'
  descendants: 22
  id: 46051449
  kids: [46054027, 46053889, 46053275, 46053515, 46053002,
  score: 217
  text: 'I got DOOM running in KiCad by rendering it with 
  time: 1764108815
  title: 'Show HN: KiDoom – Running DOOM on PCB Traces'
  type: 'story'
  url: 'https://www.mikeayles.com/#kidoom'

De API retourneert 500 story-ID's en elk verhaal heeft velden zoals title, url, score en by (de auteur). 

Meerdere resultaten sequentieel vs. gelijktijdig ophalen

Laten we nu 10 verhalen sequentieel ophalen:

import aiohttp
import asyncio
import time

HN_API = "https://hacker-news.firebaseio.com/v0"

async def fetch_story(session, story_id):
    async with session.get(f"{HN_API}/item/{story_id}.json") as response:
        return await response.json()

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()
        
        start = time.perf_counter()
        stories = []
        for story_id in story_ids[:10]:
            story = await fetch_story(session, story_id)
            stories.append(story)
        elapsed = time.perf_counter() - start
        
        print(f"Sequential: Fetched {len(stories)} stories in {elapsed:.2f} seconds")

asyncio.run(main())
Sequential: Fetched 10 stories in 2.41 seconds

Laten we nu dezelfde verhalen gelijktijdig ophalen:

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()
        
        start = time.perf_counter()
        tasks = [fetch_story(session, story_id) for story_id in story_ids[:10]]
        stories = await asyncio.gather(*tasks)
        elapsed = time.perf_counter() - start
        
        print(f"Concurrent: Fetched {len(stories)} stories in {elapsed:.2f} seconds")
        print("\nTop 3 stories:")
        for story in stories[:3]:
            print(f"  - {story.get('title', 'No title')}")

asyncio.run(main())
Concurrent: Fetched 10 stories in 0.69 seconds

Top 3 stories:
  - Show HN: KiDoom – Running DOOM on PCB Traces
  - AWS is 10x slower than a dedicated server for the same price [video]
  - Surprisingly, Emacs on Android is pretty good

De gelijktijdige versie is 3,5x sneller. In plaats van te wachten tot elk verzoek klaar is voordat het volgende start, draaien alle 10 verzoeken tegelijk. Hier betaalt async programmeren zich uit bij echte netwerk-I/O.

Foutafhandeling en rate limiting bij asynchroon Python

Bij gelijktijdig data ophalen kan er van alles misgaan. Je kunt de server overspoelen met te veel verzoeken. Sommige verzoeken blijven eeuwig hangen. Andere mislukken meteen. En als er fouten optreden, heb je een herstelstrategie nodig.

Deze sectie doorloopt elke zorg in de volgorde waarin ze voorkomen: regelen hoeveel verzoeken eruit gaan, tijdslimieten instellen, fouten afhandelen en opnieuw proberen waar dat logisch is. Heb je een opfrisser nodig over de basis van exception handling in Python? Zie Exception & Error Handling in Python. We gebruiken deze basisopzet door de sectie heen:

import aiohttp
import asyncio
import time

HN_API = "https://hacker-news.firebaseio.com/v0"

async def fetch_story(session, story_id):
    async with session.get(f"{HN_API}/item/{story_id}.json") as response:
        return await response.json()

Rate limiting met semaforen

In de vorige sectie stuurden we 10 verzoeken tegelijk. Dat ging prima. Maar wat gebeurt er als je 500 verhalen moet ophalen? Of 10.000 pagina's scrapen?

De meeste API's hanteren rate limits. Ze staan bijvoorbeeld 10 verzoeken per seconde toe, of 100 gelijktijdige verbindingen. Overschrijd je die limieten, dan word je geblokkeerd, afgeremd of verbannen. Zelfs als de API geen limieten afdwingt, kan het afvuren van duizenden verzoeken tegelijk je eigen systeem of de server overbelasten.

Je hebt een manier nodig om te regelen hoeveel verzoeken er op elk moment "in de lucht" zijn. Dat is wat een semafoor doet.

Een semafoor werkt als een vergunningensysteem. Stel dat je drie vergunningen hebt. Elke taak die een verzoek wil doen, moet eerst een vergunning krijgen. Als hij klaar is, geeft hij de vergunning terug, zodat een nieuw verzoek die kan gebruiken. Als er geen vergunningen beschikbaar zijn, wacht de taak tot er één vrijkomt.

Async-semafoor voor het beheren van gelijktijdigheid met vergunningen.

Zo speelt het zich af met 3 vergunningen en 4 of meer taken:

  1. Er zijn drie vergunningen beschikbaar.

  2. Taak A neemt een vergunning (nog 2), start zijn verzoek.

  3. Taak B neemt een vergunning (nog 1), start zijn verzoek.

  4. Taak C neemt een vergunning (nog 0), start zijn verzoek.

  5. Taak D wil een vergunning, maar er is geen beschikbaar—die wacht.

  6. Taak A is klaar, geeft zijn vergunning terug (1 beschikbaar).

  7. Taak D neemt die vergunning en start zijn verzoek.

  8. Dit gaat zo door tot alle taken klaar zijn.

Het wachten in stap 5 is efficiënt. De taak draait niet in een lusje "is er al een vergunning vrij?". Hij wordt geschorst en laat andere code draaien. De event loop wekt hem pas als er een vergunning beschikbaar komt.

Nu de code. In asyncio maak je een semafoor met asyncio.Semaphore(n), waarbij n het aantal vergunningen is. Om hem te gebruiken, wikkel je je code in async with semaphore:. Dit verkrijgt een vergunning bij binnenkomst van het blok en geeft die automatisch vrij bij het verlaten:

async def fetch_story_limited(session, story_id, semaphore):
    async with semaphore:  # Acquire permit (or wait if none available)
        async with session.get(f"{HN_API}/item/{story_id}.json") as response:
            return await response.json()
    # Permit automatically released here

Laten we 30 verhalen ophalen met en zonder semafoor:

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = (await response.json())[:30]

        # Without rate limiting: all 30 at once
        start = time.perf_counter()
        await asyncio.gather(*[fetch_story(session, sid) for sid in story_ids])
        print(f"No limit: {time.perf_counter() - start:.2f}s (30 concurrent)")

        # With Semaphore(5): max 5 at a time
        semaphore = asyncio.Semaphore(5)
        start = time.perf_counter()
        await asyncio.gather(*[fetch_story_limited(session, sid, semaphore) for sid in story_ids])
        print(f"Semaphore(5): {time.perf_counter() - start:.2f}s (5 concurrent)")

asyncio.run(main())
No limit: 0.62s (30 concurrent)
Semaphore(5): 1.50s (5 concurrent)

De versie met semafoor is langzamer omdat die verzoeken in groepjes van vijf verwerkt. Maar dat is de afweging: je levert snelheid in voor voorspelbaar, servervriendelijk gedrag.

Let op: een semafoor limiteert gelijktijdige verzoeken, niet verzoeken per tijdseenheid. Semaphore(10) betekent "maximaal 10 verzoeken tegelijk", niet "10 verzoeken per seconde". Als je strikte, tijdgebaseerde rate limiting nodig hebt (zoals exact 10 verzoeken per seconde), kun je een semafoor combineren met pauzes tussen batches, of een bibliotheek zoals aiolimiter gebruiken.

Timeouts met asyncio.wait_for()

Zelfs met gecontroleerde gelijktijdigheid kunnen individuele verzoeken blijven hangen. Een server kan je verbinding accepteren maar nooit antwoorden. Zonder timeout wacht je programma eindeloos.

De functie asyncio.wait_for() omhult elke coroutine met een deadline. Je geeft de coroutine en een timeout in seconden. Als de operatie niet op tijd klaar is, wordt asyncio.TimeoutError opgegooid:

async def slow_operation():
    print("Starting slow operation...")
    await asyncio.sleep(5)
    return "Done"

async def main():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=2.0)
        print(f"Success: {result}")
    except asyncio.TimeoutError:
        print("Operation timed out after 2 seconds")

asyncio.run(main())
Starting slow operation...
Operation timed out after 2 seconds

Wanneer de timeout verloopt, cancelt wait_for() de coroutine. Je kunt TimeoutError opvangen en beslissen wat je doet: het verzoek overslaan, een standaardwaarde teruggeven of opnieuw proberen.

Voor gelijktijdige verzoeken wikkel je elk verzoek afzonderlijk in. Hier is een helper die een fout-dictionary teruggeeft in plaats van een error op te werpen:

async def fetch_story_with_timeout(session, story_id, timeout=5.0):
    try:
        coro = fetch_story(session, story_id)
        return await asyncio.wait_for(coro, timeout=timeout)
    except asyncio.TimeoutError:
        return {"error": f"Story {story_id} timed out"}

Wanneer een coroutine wordt geannuleerd (door een timeout of een andere reden), geeft Python asyncio.CancelledError op binnen die coroutine. Als je coroutine resources vasthoudt zoals bestandsdescriptors of verbindingen, gebruik dan try/finally om te garanderen dat opschonen gebeurt, zelfs bij annulering:

async def fetch_with_cleanup(session, url):
    print("Starting fetch...")
    try:
        async with session.get(url) as response:
            return await response.text()
    finally:
        print("Cleanup complete")  # Runs even on cancellation

Foutafhandeling met asyncio.gather()

Timeouts vangen trage verzoeken. Maar sommige verzoeken mislukken meteen met een fout. Laten we kijken wat er gebeurt wanneer één verzoek in een batch faalt.

Eerst hebben we een versie van fetch_story() nodig die een exceptie opwerpt bij ongeldige ID's:

async def fetch_story_strict(session, story_id):
    story = await fetch_story(session, story_id)
    if story is None:
        raise ValueError(f"Story not found: {story_id}")
    return story

Laten we nu vier geldige verhalen plus één ongeldige ID ophalen:

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()

        ids_to_fetch = story_ids[:4] + [99999999999]  # 4 valid + 1 invalid

        try:
            stories = await asyncio.gather(
                *[fetch_story_strict(session, sid) for sid in ids_to_fetch]
            )
            print(f"Got {len(stories)} stories")
        except ValueError as e:
            print(f"ERROR: {e}")

asyncio.run(main())
ERROR: Story not found: 99999999999

Met één ongeldige ID verliezen we alle vier de geslaagde resultaten. Standaard gebruikt gather() fail-fast-gedrag: één exceptie cancelt alles en wordt omhoog doorgegeven.

Om gedeeltelijke resultaten te behouden, voeg je return_exceptions=True toe. Dit verandert het gedrag van gather(): in plaats van excepties op te werpen, retourneert het ze als items in de resultatenlijst naast de geslaagde waarden:

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()

        ids_to_fetch = story_ids[:4] + [99999999999]

        results = await asyncio.gather(
            *[fetch_story_strict(session, sid) for sid in ids_to_fetch],
            return_exceptions=True  # Don't raise, return exceptions in list
        )

        # Separate successes from failures using isinstance()
        stories = [r for r in results if not isinstance(r, Exception)]
        errors = [r for r in results if isinstance(r, Exception)]

        print(f"Got {len(stories)} stories, {len(errors)} failed")

asyncio.run(main())
Got 4 stories, 1 failed

Met de check isinstance(result, Exception) kun je geslaagde resultaten scheiden van fouten. Vervolgens kun je verwerken wat gelukt is en de mislukkingen loggen of opnieuw proberen.

Retry-logica met exponentiële backoff

Sommige fouten zijn tijdelijk. Een server kan even overbelast zijn, of een netwerk-hapering kan je verbinding verbreken. In zulke gevallen is opnieuw proberen zinvol.

Maar meteen opnieuw proberen kan het erger maken. Als een server het moeilijk heeft, vererger je het probleem door hem te bestoken met retries. Exponentiële backoff lost dit op door tussen elke poging langer te wachten.

Het patroon gebruikt 2 ** attempt om wachttijden te berekenen: poging 0 wacht één seconde (2⁰), poging 1 wacht twee seconden (2¹), poging 2 wacht vier seconden (2²), enzovoort. Dit geeft de server steeds meer tijd om te herstellen:

async def fetch_with_retry(session, story_id, max_retries=3):
    for attempt in range(max_retries):
        try:
            story = await fetch_story(session, story_id)
            if story is None:
                raise ValueError(f"Story {story_id} not found")
            return story
        except (aiohttp.ClientError, ValueError):  # Catch specific exceptions
            if attempt == max_retries - 1:
                print(f"Story {story_id}: Failed after {max_retries} attempts")
                return None

            backoff = 2 ** attempt  # 1s, 2s, 4s...
            print(f"Story {story_id}: Attempt {attempt + 1} failed, retrying in {backoff}s...")
            await asyncio.sleep(backoff)

Let op dat we specifieke excepties opvangen (aiohttp.ClientError, ValueError) in plaats van een kale except. Zo proberen we alleen opnieuw bij fouten die mogelijk tijdelijk zijn. Een KeyError door slechte code moet geen retries triggeren.

Laten we testen met een mix van geldige en ongeldige ID's:

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(f"{HN_API}/topstories.json") as response:
            story_ids = await response.json()

        test_ids = [story_ids[0], 99999999999, story_ids[1], 88888888888, story_ids[2]]

        results = await asyncio.gather(*[fetch_with_retry(session, sid) for sid in test_ids])

        successful = [r for r in results if r is not None]
        print(f"\nSuccessful: {len(successful)}, Failed: {len(test_ids) - len(successful)}")

asyncio.run(main())
Story 99999999999: Attempt 1 failed, retrying in 1s...
Story 88888888888: Attempt 1 failed, retrying in 1s...
Story 99999999999: Attempt 2 failed, retrying in 2s...
Story 88888888888: Attempt 2 failed, retrying in 2s...
Story 99999999999: Failed after 3 attempts
Story 88888888888: Failed after 3 attempts

Successful: 3, Failed: 2

In productie voeg je ook jitter toe (kleine willekeurige vertragingen) om te voorkomen dat meerdere mislukte verzoeken op exact hetzelfde moment opnieuw proberen. Daarnaast zou je alleen tijdelijke fouten (server-side netwerkproblemen, zoals 503) opnieuw proberen en meteen stoppen bij permanente fouten (bijvoorbeeld 404 of 401).

Asynchrone databaseopslag in Python met aiosqlite

We hebben Hacker News-verhalen opgehaald met de juiste rate limiting, timeouts en foutafhandeling. Nu slaan we ze op in een database.

Een reguliere synchrone databasebibliotheek zoals sqlite3 zou de event loop blokkeren tijdens queries, wat het doel van asynchroon programmeren ondermijnt. Terwijl je code op de database wacht, kunnen er geen andere coroutines draaien. Voor async toepassingen heb je een async databasebibliotheek nodig.

aiosqlite wikkelt Python's ingebouwde sqlite3 in een async-interface. Het draait databaseoperaties in een threadpool zodat ze de event loop niet blokkeren. SQLite vereist geen serverconfiguratie—het is gewoon een bestand—dus je kunt deze code meteen draaien. Als je nieuw bent met databases in Python, behandelt de cursus Introduction to Databases in Python de synchrone basis waar aiosqlite op voortbouwt.

De database opzetten

Het patroon zal bekend voorkomen. Net als aiohttp.ClientSession gebruik je async with om de verbinding te beheren:

import aiosqlite

async def init_db(db_path):
    async with aiosqlite.connect(db_path) as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS stories (
                id INTEGER PRIMARY KEY,
                title TEXT,
                url TEXT,
                score INTEGER,
                fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        await db.commit()

asyncio.run(init_db("stories.db"))

De belangrijkste functies:

  • aiosqlite.connect(path) opent (of maakt) een databasebestand.

  • await db.execute(sql) voert een SQL-statement uit.

  • await db.commit() slaat wijzigingen op de schijf op.

Verhalen opslaan

Hier is een functie om één verhaal op te slaan:

async def save_story(db, story):
    await db.execute(
        "INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",
        (story["id"], story.get("title", ""), story.get("url", ""), story.get("score", 0))
    )

De ?-plaatsaanduidingen voorkomen SQL-injectie—gebruik nooit f-strings om waarden in SQL te plaatsen. INSERT OR REPLACE werkt bestaande verhalen bij als we ze opnieuw ophalen.

Complete async-pijplijn in Python: ophalen en opslaan

Laten we nu alles uit deze tutorial combineren tot één complete pijplijn. We halen 20 Hacker News-verhalen op met rate limiting en slaan ze op in een database:

import aiohttp
import aiosqlite
import asyncio

HN_API = "https://hacker-news.firebaseio.com/v0"

async def fetch_story(session, story_id):
    async with session.get(f"{HN_API}/item/{story_id}.json") as response:
        return await response.json()

async def fetch_story_limited(session, story_id, semaphore):
    async with semaphore:
        story = await fetch_story(session, story_id)
        if story:
            return story
        return None

async def save_story(db, story):
    await db.execute(
        "INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",
        (story["id"], story.get("title", ""), story.get("url", ""), story.get("score", 0))
    )

async def main():
    # Initialize database
    async with aiosqlite.connect("hn_stories.db") as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS stories (
                id INTEGER PRIMARY KEY,
                title TEXT,
                url TEXT,
                score INTEGER,
                fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        
        # Fetch stories
        async with aiohttp.ClientSession() as session:
            async with session.get(f"{HN_API}/topstories.json") as response:
                story_ids = await response.json()
            
            semaphore = asyncio.Semaphore(5)
            tasks = [fetch_story_limited(session, sid, semaphore) for sid in story_ids[:20]]
            stories = await asyncio.gather(*tasks)
        
        # Save to database
        for story in stories:
            if story:
                await save_story(db, story)
        await db.commit()
        
        # Query and display
        cursor = await db.execute("SELECT id, title, score FROM stories ORDER BY score DESC LIMIT 5")
        rows = await cursor.fetchall()
        
        print(f"Saved {len([s for s in stories if s])} stories. Top 5 by score:")
        for row in rows:
            print(f"  [{row[2]}] {row[1][:50]}")

asyncio.run(main())
Saved 20 stories. Top 5 by score:
  [671] Google Antigravity exfiltrates data via indirect p
  [453] Trillions spent and big software projects are stil
  [319] Ilya Sutskever: We're moving from the age of scali
  [311] Show HN: We built an open source, zero webhooks pa
  [306] FLUX.2: Frontier Visual Intelligence

De pijplijn gebruikt patronen uit elke sectie: ClientSession voor connection pooling, Semaphore(5) voor rate limiting, gather() voor gelijktijdig ophalen en nu aiosqlite voor asynchrone opslag. Elk onderdeel handelt zijn deel af zonder de anderen te blokkeren.

Elke keer dat je deze workflow draait, ontvang je de topverhalen van de dag.

Conclusie

Deze tutorial nam je mee van de basis-syntaxis van async/await naar een complete datapijplijn. Je leerde hoe coroutines pauzeren en hervatten, hoe de event loop gelijktijdige taken beheert en hoe asyncio.gather() meerdere operaties tegelijk uitvoert. Je voegde echte HTTP-verzoeken toe met aiohttp, regelde gelijktijdigheid met semaforen, handelde fouten af met timeouts en retries, en sloeg resultaten op in een database met aiosqlite.

Gebruik async wanneer je code wacht op externe systemen: HTTP-API's, databases, bestands-I/O of netwerk-sockets. Voor CPU-zwaar werk zoals dataverwerking of number crunching helpt async niet—kijk dan naar multiprocessing of concurrent.futures. Wil je verder de diepte in, bekijk dan de asyncio-documentatie en overweeg FastAPI voor het bouwen van async web-API's. 

Wil je voortbouwen op deze kennis en leren hoe je intelligente applicaties ontwerpt? Bekijk dan de Associate AI Engineer for Developers-carrièreroute.

Veelgestelde vragen over Python async

Wat is het verschil tussen async en sync programmeren in Python?

Bij synchroon programmeren voert code één regel tegelijk uit en wacht op elke operatie tot die klaar is. Async programmeren laat je code een operatie starten en doorgaan met ander werk tijdens het wachten, om vervolgens te hervatten wanneer het resultaat klaar is. Dit wordt beheerd door een event loop die tussen taken schakelt.

Wanneer moet ik async programmeren gebruiken in plaats van regulier Python?

Gebruik async voor I/O-gebonden taken waarbij je code wacht op externe systemen: HTTP-verzoeken, databasequeries, bestandsoperaties of netwerk-sockets. Async helpt niet bij CPU-gebonden werk zoals dataverwerking of berekeningen—gebruik daarvoor multiprocessing of concurrent.futures.

Waarom krijg ik een waarschuwing "coroutine was never awaited"?

Dit gebeurt wanneer je een async functie aanroept zonder await te gebruiken. Het aanroepen van een async functie zoals get_data() retourneert een coroutine-object, niet het resultaat. Je moet await get_data() gebruiken om hem echt uit te voeren en de returnwaarde te krijgen.

Kan ik de requests-bibliotheek gebruiken met asyncio?

Nee, de bibliotheek requests is synchroon en blokkeert de event loop, wat het doel van async ondermijnt. Gebruik in plaats daarvan aiohttp—dat is een async HTTP-client ontworpen voor gelijktijdige verzoeken. Vergeet niet één enkele ClientSession te hergebruiken voor connection pooling.

Hoe beperk ik gelijktijdige verzoeken om te voorkomen dat ik een API overweldig?

Gebruik asyncio.Semaphore om te regelen hoeveel verzoeken er tegelijk draaien. Maak een semafoor met je gewenste limiet (bijv. asyncio.Semaphore(5)) en wikkel elk verzoek in async with semaphore. Zo zijn er nooit meer dan dat aantal verzoeken tegelijk "in de lucht".


Bex Tuychiev's photo
Author
Bex Tuychiev
LinkedIn

Ik ben een contentmaker op het gebied van data science met meer dan 2 jaar ervaring en een van de grootste achterbannen op Medium. Ik schrijf graag diepgaande artikelen over AI en ML met een vleugje sarcasme, want je moet íets doen om ze wat minder droog te maken. Ik heb meer dan 130 artikelen en een DataCamp-cursus gemaakt, met nog een in de maak. Mijn content is door meer dan 5 miljoen ogen bekeken, van wie 20k mij is gaan volgen op zowel Medium als LinkedIn. 

Onderwerpen

Python-cursussen

Leerpad

Associate AI Engineer voor ontwikkelaars

26 Hr
Leer hoe je AI in softwareapplicaties kunt integreren met behulp van API's en open-sourcebibliotheken. Begin vandaag nog aan je reis om AI-ingenieur te worden!
Bekijk detailsRight Arrow
Begin met de cursus
Meer zienRight Arrow
Gerelateerd

blog

AI vanaf nul leren in 2026: een complete gids van de experts

Ontdek alles wat je moet weten om in 2026 AI te leren, van tips om te beginnen tot handige resources en inzichten van industrie-experts.
Adel Nehme's photo

Adel Nehme

15 min

Meer zienMeer zien