Gebruik de guard die iedereen schrijft:
try:
r = requests.get(url, timeout=0.5)
except requests.exceptions.Timeout:
retry()
Richt die op een server die meteen de statusregel en headers terugstuurt en daarna vastloopt vóór de body. Dan loopt de read time-out af. De guard wordt niet getriggerd. De uitzondering die terugkomt is ConnectionError, en ConnectionError is geen subclass van Timeout.
Bij dezelfde storing geeft httpx ReadTimeout, en dat is wél een TimeoutException. Die wordt door de equivalente guard dus netjes opgevangen.
Ik ging op zoek naar de verschillen tussen httpx en requests op punten die scrapers kunnen breken, met het idee dat ik vooral over async zou schrijven. Uiteindelijk bleek async het minst interessante onderwerp op de lijst.
Wat ik testte en hoe
Acht probes op een lokale testserver, omdat wat een client over zichzelf zegt geen bewijs is van wat hij echt heeft gedaan. De server telt TCP-verbindingen — telkens één keer omhoog bij een geaccepteerde socket, nog vóór er een requestregel wordt geparsed — en daadwerkelijk opgehaalde paden. Hergebruik van verbindingen en het volgen van redirects zijn allebei netwerkclaims, en juist daar kun je ze controleren.
httpx 0.28.1 met de http2-extra, requests 2.34.2, Python 3.14.2, macOS arm64. Allebei draaiden ze in één verse virtualenv, zodat geen van beide iets van de ander erft. Ruwe output: httpx-probes.json.
Zes voorspellingen gingen vóór de eerste run het harness in en bleven daarna staan. Drie klopten, twee niet, één zat goed op het geval dat ik had bedacht maar miste het geval dat er echt toe deed. prediction-scorecard.json bevat de afrekening.
De defaults die onder je voeten verschuiven

| Gedrag | requests 2.34.2 | httpx 0.28.1 |
|---|---|---|
| Volgt redirects standaard | ja | nee |
| Module-level aanroep hergebruikt verbindingen | nee | nee |
| Vastlopen vóór headers | ReadTimeout | ReadTimeout |
| Vastlopen midden in de body | ConnectionError | ReadTimeout |
| Geen charset gedeclareerd | ISO-8859-1 | utf-8 |
| HTTP/2 | niet beschikbaar | opt-in, werkt |
| Gescheiden connect/read/write/pool timeouts | nee | ja |
Redirects, sockethergebruik, uitzonderingsuitkomsten, decodering en protocolnegotiatie zijn geobserveerd in probes. De vorm van de timeout-API en het ontbreken van een HTTP/2-vlag in requests zijn observaties over de API-mogelijkheden. httpx-probes.json.
Drie van deze regels veranderen stilletjes wat je code doet op de dag dat je overstapt.
Redirects: standaard uit, en de server bewijst het
Een redirectketen van vier stappen die eindigt op /ok:
| Client | Server zag | Geretourneerde status |
|---|---|---|
| requests | 5 requests | 200 |
| httpx | 1 request | 302 |
httpx, follow_redirects=True | 5 requests | 200 |
Die vijf is vier stappen plus de bestemming. Mijn voorspelling zei vier, wat rekenwerk was dat ik niet had gecontroleerd; de richting was de claim en het aantal is hier gecorrigeerd in plaats van stilletjes in de tekst.
Dit is gedocumenteerd gedrag van httpx en een verdedigbare keuze — een redirect is iets waar de aanroeper misschien iets van wil weten. Het is ook de meest waarschijnlijke manier waarop een migratie stukloopt zonder dat er een uitzondering wordt gegooid. Je code krijgt een 302, response.text is leeg, je parser vindt geen rijen, en je logs zeggen 200 OK… behalve dat ze 302 zeggen, en niemand lette op de statuscode omdat er met requests nooit iets was om op te letten.
De timeoutbevinding, en dit is de ene waar ik het mis had
Ik voorspelde dat httpx de mislukte fase zou benoemen en dat requests beide gevallen in één klasse zou laten opgaan. Het is precies andersom.
Officiële verwijzing: Requests timeout-documentatie.

Officiële verwijzing: HTTPX timeout-documentatie.
| Storing | requests | httpx |
|---|---|---|
| Vóór de statusregel | ReadTimeout | ReadTimeout |
| Midden in de body, nadat headers al zijn verzonden | ConnectionError | ReadTimeout |
httpx geeft voor beide gevallen dezelfde, correcte naam. requests splitst ze — precies over de grens waar retry-code op is geschreven.
Het gevolg hoef je niet uit de class hierarchy af te leiden. Ik heb de guard gewoon uitgevoerd:
| Storing | except requests.exceptions.Timeout | except httpx.TimeoutException |
|---|---|---|
| Vóór de statusregel | vangt af | vangt af |
| Midden in de body | glipt weg als ConnectionError | vangt af |
timeout-retry-guard.json. requests.exceptions.ConnectionError is geen subclass van requests.exceptions.Timeout; httpx.ReadTimeout is wel een subclass van httpx.TimeoutException.
De foutmelding van requests zegt Read timed out. binnen een ConnectionError. De bibliotheek weet dus wat er gebeurde. Alleen geeft hij het niet door aan het type-systeem, en juist dat type-systeem kijkt je except-clausule aan.
Het gemeten geval is specifiek: headers komen binnen, daarna stopt de voortgang van de body lang genoeg om de read timeout te overschrijden. Een response die binnen het timeoutvenster steeds chunks blijft leveren, bijvoorbeeld een bewuste stream, kan anders reageren en is hier niet getest.
Connection pooling: de client-API maakt het verschil
Tien GET-requests, vier manieren, sockets geteld aan serverzijde:
| Manier | Geopende sockets |
|---|---|
httpx.get() × 10 | 10 |
requests.get() × 10 | 10 |
httpx.Client() | 1 |
requests.Session() | 1 |
Identiek, en het is goed om dat expliciet te zeggen, omdat dit de meest gehoorde folk knowledge over deze twee libraries is — dat httpx pooled en requests niet. Geen van beide pooled op module-niveau. Beide poolen via hun client-object. Als je vandaag requests.get() in een loop gebruikt, verandert overstappen naar httpx.get() in een loop niets aan je socket churn.

HTTP/2 is expliciet en vereist de extra
Tegen één publiek HTTP/2-endpoint dat in het artefact is vastgelegd:
Officiële verwijzing: RFC 9113: HTTP/2.
| Client | Onderhandeld protocol |
|---|---|
httpx.Client(http2=True) | HTTP/2 |
httpx.Client(http2=False) | HTTP/1.1 |
| requests | HTTP/1.1, er bestaat geen vlag |
Je hebt de httpx[http2]-extra nodig. Ik ging ervan uit dat een gewone pip install httpx je een client zou geven die stilletjes 1.1 onderhandelt, en heb dat vóór het schrijven even gecontroleerd:
ImportError: Using http2=True, but the 'h2' package is not installed.
Make sure to install httpx using `pip install httpx[http2]`.
Hij gooit een fout bij het aanmaken van de Client, nog vóór één enkele request, en de melding noemt meteen de oplossing. Dat is de goede versie van deze fout, en ik had het andersom verwacht (http2-extra-missing.json).
Deze probe laat zien dat protocolnegotiatie op dat endpoint werkt. Ze bewijst geen voordeel in scraping-snelheid; er is geen vergelijkbare HTTP/1.1-workload getest.
Sequentiële versus gelijktijdige doorvoer op de testserver
Twintig requests naar een endpoint dat 0,3 s slaapt:
| Modus | Totale tijd | Sockets |
|---|---|---|
Sync, één Client | 6.138 s | 1 |
Async, één AsyncClient | 0.357 s | 20 |
De gelijktijdige run was klaar in 0,357 seconden tegenover 6,138 seconden voor de sequentiële run. Hij opende ook twintig verbindingen terwijl de synchrone client er één hergebruikte, dus dit experiment verandert het uitvoeringsmodel en de effectieve paralleliteit in plaats van dat het puur library-snelheid isoleert.
Dat is de eerlijke lezing van het getal. Het is een meting van concurrency tegen een bewust traag endpoint, geen meting van httpx zelf. Elke client met een werkend async-verhaal komt ongeveer in dezelfde buurt, en tegen een snel endpoint verdwijnt het verschil.
Het charset-geval dat ik niet dacht te voorspellen
Ik voorspelde dat een response waarvan de header liegt — charset=iso-8859-1 op utf-8-bytes — in beide gevallen dezelfde mojibake zou geven. Dat klopt. Beide geven Café Ubersetzung â naïve résumé terug terwijl de bron Café Ubersetzung — naïve résumé zegt.
Het geval dat ik níet voorspelde, is het geval dat ertoe doet:
| Response | requests decodert | httpx decodert |
|---|---|---|
charset=utf-8, utf-8-bytes | correct | correct |
charset=iso-8859-1, utf-8-bytes | mojibake | mojibake |
| geen charset | mojibake | correct |
requests valt terug op ISO-8859-1 wanneer de header niets zegt, terwijl httpx standaard utf-8 gebruikt. In de test zonder charset leverden de clients daarom verschillende gedecodeerde tekst op via .text; gebruikers van response.content behouden gewoon dezelfde originele bytes.
Geheugen, omdat dat goedkoop te meten is
Peak RSS, /usr/bin/time -l, per cel één vers proces:
| Cel | requests | httpx |
|---|---|---|
| Alleen import | 36.0 MiB | 30.6 MiB |
| Import plus één GET | 35.8 MiB | 40.7 MiB |
Dit zijn snapshots van één proces, en het feit dat requests’ één-GET-waarde iets onder de import-only-waarde ligt, laat vooral ruis in de run zien. Hieruit kun je geen harde conclusie over geheugen trekken; daarvoor heb je herhaalde samples en spreiding nodig.
Wat dit betekent bij de keuze
Bugs zoeken in bestaande requests-code? Controleer aannames over redirects, handlers die alleen requests.exceptions.Timeout afvangen maar denken ook een mid-body-storing te dekken, en .text-consumenten die responses zonder charset krijgen.
Mechanisch migreren naar httpx? Pas exception-namespaces aan naar httpx.TimeoutException of de fijnere faseklassen, bepaal of je follow_redirects wilt aanzetten, en test aannames over decodering opnieuw. De bestaande requests-handler mist het getoonde mid-body-geval al; migratie creëert die specifieke bug niet.
Iets nieuws schrijven dat veel URL's ophaalt? httpx is een kandidaat als je AsyncClient nodig hebt en aparte connect-, read-, write- en pool-timeouts. Die fasen laten zien waar het wachten zat — tijdens het opzetten van de verbinding, het binnenhalen van de response-body, het uploaden van de request of het krijgen van een vrije pool-slot — niet waarom een remote host zich zo gedroeg.
Iets kleins en synchroon schrijven? requests is prima en overal aanwezig. De reden om over te stappen is niet snelheid.
Wat je ook kiest, gebruik het client-object in plaats van de module-level functie. Dat is de ene verandering op deze lijst die in beide libraries een duidelijke winst oplevert.
Waar een managed API past
Alles hierboven zit op de fetch-laag, en dat is het makkelijke deel. Niets hiervan rendert JavaScript, niets handelt een anti-bot challenge af, en niets zet HTML om in de rijen die je wilde hebben.
Auteur opmerking: Thunderbit is onze managed optie voor URL-in rendering en extractie. Die is niet getest in dit HTTP-client-harness. Zie dat soort oplossing alleen als het ophalen van pagina’s of gestructureerde extractie — niet de semantiek van de HTTP-client — het probleem is dat je wilt oplossen.
Als je gewoon pagina’s ophaalt en zelf parseert, blijven beide clients relevant. Een managed service is een aparte build-versus-buy-beslissing, geen bewijs voor een keuze tussen deze libraries.
Voor het bredere veld behandelt onze overzichtspagina voor webscraping-API's de gehoste opties en de open-source scraper-pagina de zelfgehoste.
Probeer Thunderbit voor webdata-extractie
Eindoordeel
Voor een nieuwe Python-fetch-laag die async concurrency, fase-specifieke timeouts en een UTF-8-fallback nodig heeft, is httpx onder de hier geteste voorwaarden mijn standaardkeuze. Requests blijft prima bruikbaar voor volwassen synchrone code waarbij migratierisico zwaarder weegt dan die voordelen. Proxies, retry-beleid, TLS-fingerprinting, streaming, uploads en realistische netwerkvariatie zijn niet getest, dus dit is geen universele ranglijst van scraping-clients.
De reden om voorzichtig te zijn is de redirect-default, en dat is juist een reëel risico omdat het een goede ontwerpkeuze is. Expliciet is beter dan impliciet, totdat het impliciete ding dragend bleek te zijn in code die je al hebt uitgebracht.
De vooraf geregistreerde scorecard eindigde met drie juiste voorspellingen, twee onjuiste en één onvolledige voorspelling. De nuttige correctie was de exception class bij mid-body-stalls; de rest van de beslissing moet komen uit het geobserveerde gedrag, niet uit het verhaal van de scorecard.
Probeer Thunderbit voor webdata-extractie Get Started Free
Veelgestelde vragen
Volgt httpx echt geen redirects?
Niet standaard, nee. De server telde één request voor een keten van vier hops, en de response kwam terug als een 302. Geef per call follow_redirects=True mee, of stel het één keer in op de Client. Dit is gedocumenteerd en bewust zo ontworpen; toch is het nog steeds het meest waarschijnlijke punt waarop een migratie stilletjes misgaat, omdat het falen een lege parse is in plaats van een uitzondering.
Is except requests.exceptions.Timeout echt niet genoeg?
Niet voor een server die vastloopt nadat de headers zijn verzonden. In dat geval krijg je ConnectionError, en dat is geen subclass van Timeout, dus de guard mist het — rechtstreeks aangetoond, niet afgeleid. Vang requests.exceptions.RequestException als je beide wilt meenemen, en accepteer dat je daarmee ook dingen opvangt die geen timeouts zijn.
Is httpx sneller dan requests? Niet echt, als je één request tegelijk doet — daarvoor is het niet bedoeld. De 17,2× in deze test is een meting van twintig gelijktijdige requests tegen een endpoint van 0,3 s, dus van concurrency. Als je workload sequentieel is, verwacht dan geen snelheidswinst en kies op basis van de defaults.
Heb ik de http2-extra nodig?
Alleen als je HTTP/2 wilt — en als je http2=True instelt zonder die extra, gooit httpx een ImportError bij het construeren van de Client, met een bericht dat je vertelt httpx[http2] te installeren. Geen stille downgrade om je zorgen over te maken. Ik verwachtte die wel en heb het daarom eerst gecontroleerd in plaats van het zomaar op te schrijven.
Wat is hier niet getest? Proxygedrag, en dat is erg belangrijk voor scraping en verdient een eigen harness. Retries — httpx levert geen retry-logica en requests krijgt die van urllib3, dus een eerlijke vergelijking is eigenlijk een vergelijking van twee retry-bibliotheken. TLS-fingerprinting, de as waar anti-botsystemen echt naar kijken en waar geen van beide libraries iets aan doet. Streaming en file uploads. En alles hier draaide op één machine, één Python-versie en in zes van de acht probes op localhost — latencycijfers van een testserver meten het ontwerp, niet je netwerk.


