Data scraping
Playwright vs Crawlee vs Workers: een eerlijke test
Een Rotterdamse expediteur had dagelijkse tariefsnapshots nodig van 38 carrier-portals. We bouwden de scraper op drie manieren voordat we een stack hadden waar we op konden vertrouwen.

Om 05:48 op een dinsdag in maart zat de operations lead van een middelgrote Rotterdamse expediteur op haar negende carrier-portal van die ochtend. Maersk, MSC, CMA CGM, Hapag-Lloyd, allemaal open in aparte tabs, allemaal met dezelfde zes getallen die voor haar offerte-deadline van 06:30 in dezelfde spreadsheet gekopieerd moesten worden. Ze deed dit al negen jaar. Wij werden ingeschakeld om er een einde aan te maken.
De opdracht was helder. 1.400 carrier-pagina's per dag scrapen over 38 portals, de deltas naar een Slack-kanaal en een Postgres-tabel pushen, en nooit een maandag missen waarop de spotmarkt beweegt. Het interessante zat in de spreiding. Achttien van die portals waren JavaScript-rendered achter een lichte login. Twintig waren simpele PHP-pagina's die voor het laatst in 2014 waren herontworpen.
We bouwden 'm drie keer voordat we live gingen.
Ronde één: Playwright overal
De eerste reflex was één fleet van Playwright-workers. Eén framework, één mentaal model, iedere carrier op dezelfde manier behandeld. We zetten vier EC2 t3.large-boxes op met een Postgres-queue, een kleine Node-orchestrator en headless Chromium. Op dag één werkte het. Op dag drie hadden we problemen.
Geheugen was de eerste muur. Elke Chromium-context hield 180 tot 260 MB vast, en contexts roteren om fingerprinting te ontwijken kostte minuten per carrier. De statische HTML-pagina's deden er 4 tot 7 seconden over, omdat we volledige browser-boot-tax betaalden op een pagina die in 80 ms met fetch en cheerio geparseerd had kunnen worden. De compute-rekening kwam op zo'n €310 per maand, voordat we retries of een echte proxy hadden toegevoegd.
Playwright was de juiste keuze voor de achttien lastige portals. Voor de andere twintig was het theatrale overkill.
Ronde twee: Crawlee in het midden
Crawlee is het open-source crawling-framework van Apify, en wat ze goed doen is dat je niet vooraf één strategie hoeft te kiezen. Je mengt een CheerioCrawler voor de statische pagina's met een PlaywrightCrawler voor de rendered exemplaren, deelt een request-queue tussen beide, en hergebruikt dezelfde retry-, proxy- en session-pool-logica.
De herbouw op Crawlee kostte vier dagen. De winst zat in operations, niet in architectuur. Mislukte requests werden nu opnieuw geprobeerd met exponentiële back-off en roterende sessions, in plaats van de worker te laten crashen. De request-queue gaf ons natuurlijke rate limiting per hostname. Zet maxConcurrencyPerHostname: 2 en je stopt met Hapag-Lloyd richting een 429 te beuken. De gemiddelde headless-tijd op de statische pagina's daalde van 5,2 s naar 240 ms, omdat we ze naar Cheerio routeerden in plaats van Chromium.
Crawlee is niet sneller dan wat je zelf zou schrijven. Het is sneller dan wat je zelf zou debuggen om elf uur 's avonds op zondag.
Ronde drie: Cloudflare Workers voor de saaie zestig procent
Voor de statische carriers, de zestig procent van het paginavolume zonder hoofdpijn, vroegen we ons af of we überhaupt een Node-runtime nodig hadden. Dus probeerden we een zelfgeschreven scraper op Cloudflare Workers, met HTMLRewriter, een D1-database voor de queue en Cron Triggers die elk kwartier afgaan.
De code was korter dan de deployment-YAML.
export default {
async scheduled(event, env) {
const targets = await env.DB
.prepare("SELECT id, url FROM carriers WHERE rendered = 0")
.all();
for (const row of targets.results) {
const res = await fetch(row.url, {
cf: { cacheTtl: 0 },
headers: { "user-agent": "abn-tariff-monitor/1.4 (+info@abn.company)" }
});
const html = await res.text();
const rates = extractRates(html); // ~40 lines of HTMLRewriter
await env.DB
.prepare("INSERT INTO tariff_snapshots(carrier_id, payload, at) VALUES (?,?,?)")
.bind(row.id, JSON.stringify(rates), Date.now())
.run();
}
}
};
Twintig carriers, 840 pagina's per dag, de hele sweep is binnen twaalf seconden klaar. De Workers free tier dekte het. Het subsysteem kost ons nul euro per maand.
Maar Workers draaien geen echte browser. De CPU-tijdlimiet (50 ms per invocation op het free plan, langer op paid) en het ontbreken van Node-API's betekenen dat elke carrier die zijn tarieven via XHR na DOMContentLoaded inlaadt, onzichtbaar voor je is. We probeerden het op twee van die portals na te bootsen en gaven na een dag op.
Cloudflare D1 kent een write-quota per minuut. Fan-out 800 INSERTs in één cron-invocation en je loopt er tegenaan. Batch ze in één prepared statement met batch(), of zet de writes via een Queues-binding in de wachtrij.
Hoe vier weken productie eruitzagen
Cijfers uit de eerste vier weken na de hybride live-gang:
- Statische pagina's op Workers: 99,6% succesratio, €0 per maand, gemiddelde latency 410 ms.
- Rendered pagina's op Crawlee plus Playwright, op één Hetzner CPX31-box (€16 per maand): 97,1% succesratio, gemiddelde latency 6,4 s.
- Totaal gescrapete pagina's per dag: 1.418. Totaal compute per maand: €16, plus zo'n €4 aan residential proxy voor de vier portals die ons fingerprintten.
De Playwright-overal-stack waarmee we begonnen, had €310 per maand gekost om hetzelfde werk te doen, en dan ook nog met lagere betrouwbaarheid. Het punt is niet dat Playwright slecht is. Het punt is dat browser-tax betalen op een statische <table> precies het soort overkill is dat een jaar lang stilletjes uren en euro's wegtrekt voordat iemand het doorheeft.
De hybride die we hebben uitgerold
Drie onderdelen. Een Cloudflare Worker op een 15-minuten cron scrapet de twintig statische carriers naar D1, en pusht rate-deltas via één HTTPS-webhook door naar een Postgres-replica. Een Crawlee-project op één Hetzner-box draait de achttien rendered portals, met PlaywrightCrawler, een gedeelde session-pool en een residential proxy op de vier kieskeurige. Een kleine orchestrator leest uit beide bronnen, berekent de carrier-vs-carrier delta en post om 06:15 Amsterdamse tijd één Slack-bericht, een kwartier voordat de operations lead haar laptop opent.
Haar ochtendlijke portal-klikwerk ging van 50 minuten naar 3 minuten scrollen door een Slack-kanaal. De doorlooptijd voor offertes op spot-aanvragen zakte van 90 minuten naar zo'n 8.
Eén ding dat je vandaag kunt doen
Scrape je meer dan een paar honderd pagina's per dag, kijk dan eens welk deel daarvan écht een browser nodig heeft. Open de Network-tab in Chrome DevTools op één van je targets, zet JavaScript uit in dezelfde tab en herlaad. Staat de data nog steeds in de HTML, dan hoort die pagina op fetch en cheerio thuis, niet op Playwright. De meeste teams waar we mee werken besparen 60 tot 80 procent van hun scraping-infrastructuur met die ene audit.
Toen we deze tariff-monitor voor de Rotterdamse expediteur bouwden, was de verrassing hoeveel waarde uit de process automation rondom de scraper kwam, en niet uit de scraper zelf. De Slack-digest, de Postgres-replica en de alert wanneer de HTML-structuur van een carrier verschuift, deden uiteindelijk zeventig procent van het werk dat de werkdag terugwon.
Kern
De meeste scrapers betalen browser-tax op pagina's die het niet nodig hebben. Bewaar Playwright voor portals die echt JavaScript draaien; gebruik fetch en een HTML-parser voor de rest.
FAQ
Wanneer pak je Playwright en wanneer een zelfgeschreven Worker?
Staat de data in de HTML nadat je JavaScript uitschakelt, gebruik dan fetch en een HTML-parser. Pak Playwright pas wanneer de pagina zijn echte data via XHR na DOMContentLoaded inlaadt.
Werkt Crawlee zonder het betaalde platform van Apify?
Ja. Crawlee is open source en draait overal waar Node draait. Het Apify-platform is een gehoste execution-laag; het framework zelf kent geen lock-in en geen licentiekosten.
Hoe vang je het op als een carrier-portal zijn HTML aanpast?
Wikkel elke extractor in een schema-check die het aantal rijen, de aanwezigheid van kolommen en de waardetypes valideert. Laat de run hardop falen en stuur een alert naar Slack, zodat je het fixt voordat iemand een offerte opnieuw beprijst op verouderde data.
Waarom Playwright niet gewoon op Cloudflare Workers via Browser Rendering draaien?
Browser Rendering werkt, maar de kosten per sessie en de cold-start-tijden voor 600 rendered pagina's per dag tillen het boven een Hetzner-box van €16 uit die Crawlee plus Playwright met gecachte browser-contexts draait.