Dashboard of traffic jams, speeds and roadworks
A dashboard showing every minute how fast traffic moves on the motorways, where it is at a standstill and which roads are closed, from NDW's open files, with the speed limit from the signs on site.
geef_project("files-en-wegwerkzaamheden")geef_bron("ndw-verkeer")geef_bron("ndw-wegkenmerken")
I have the project, the tested calls and the pitfalls. I'll build a dashboard with NDW live traffic data and Traffic signs and accessibility map (NDW). This is how it could look:
You build this yourself. Copy the prompt into Claude, ChatGPT or Cursor, with the MCP server on, or follow the steps below.
“Due to a border check” · signs within 2 km: 4× 100 on the Rijksweg A1
Motorways now
- A1111
- A12109
- A15108
- A2107
km/h, average over measuring points
1 of the 6,264 points below 50
This is a mockup, not a working version, with the figures from our test run on Friday 16 October 2026, 23:46 (NDW files and traffic sign API). Turn the MCP server on with the address on For AI.
Who it's for
Regional media and traffic reporters, municipalities and provinces planning roadworks, and anyone who wants to build their own traffic app without a paid service.
What you get
A dashboard with a map of the motorways (measuring points coloured by speed), a list of jams and incidents, the number of closures and roadworks in force now, and for each jam the speed limit according to the signs on site. Everything refreshes every minute.
Test run on Friday 16 October 2026 at 23:46: 6,264 Rijkswaterstaat measuring points on the main carriageway recorded traffic; only 1 of them was below 50 km/h (A2 at km 251.1, 10 km/h). On the A2 the average speed over 820 points was 107 km/h. The live situation file had 1,508 messages, 1,498 of them in force at that moment: 1,069 closures and lane measures (734 carriageway closures), 157 diversions, 147 temporary speed limits, 113 stranded vehicles, 2 accidents and 2 traffic jams. One of the two jams (2,100 metres, 5 minutes delay) was at the German border (52.31 N, 7.01 E) 'due to a border check'; within 2 km there were 29 speed-limit signs, including 4 times 100 km/h on the Rijksweg A1.
How you could start6 steps
- 1
Pick the small files
opendata.ndw.nu is a plain folder. This dashboard needs three files that stay under 4 MB together: snelheden_en_intensiteiten_meetgegevens.xml.gz (1.0 MB), actueel_beeld.xml.gz (474 kB), and once snelheden_en_intensiteiten_configuratie_meetlocaties.xml.gz (1.8 MB, the location of each measuring point). Leave planningsfeed_wegwerkzaamheden_en_evenementen (16 MB) and the OCPI files alone. NDW is moving to DATEX II version 3: the old files trafficspeed.xml.gz, measurement.xml.gz and traveltime.xml.gz stop on 9 February 2027.
https://opendata.ndw.nu/
Per 9 februari 2027 stopt NDW met het aanbieden van de Actuele Verkeersgegevens (AVG) in DATEX II v2.3. … actueel_beeld.xml.gz 2026-10-16 23:44 474K snelheden_en_intensiteiten_configuratie_meetlocaties.xml.gz 2026-10-16 23:44 1.8M snelheden_en_intensiteiten_meetgegevens.xml.gz 2026-10-16 23:44 1.0M tijdelijke_verkeersmaatregelen_afsluitingen.xml.gz 2026-10-16 23:44 380K tijdelijke_verkeersmaatregelen_maximum_snelheden.xml.gz 2026-10-16 23:44 40K
- 2
Read the speeds per measuring point
A new file every minute: 1 MB gzip, but 70 MB XML. Per measuring point (20,573 in total, 9,501 from Rijkswaterstaat) each lane alternates flow (vehicles per hour) and average speed (km/h). Speed -1 means: no measurement. Take an average weighted by flow. An RWS point's name tells you where it is: 0021hrl0403ra = road 002 (A2), always a 1, main carriageway left, km 40.3.
https://opendata.ndw.nu/snelheden_en_intensiteiten_meetgegevens.xml.gz
<roa:measurementSiteReference targetClass="roa:MeasurementSite" id="RWS01_MONIBAS_0011hrl0033ra" version="1742"/> <roa:physicalQuantity index="1">…<roa:basicData xsi:type="roa:TrafficFlow"><roa:vehicleFlow accuracy="0.0"> <com:vehicleFlowRate>300</com:vehicleFlowRate></roa:vehicleFlow>… <roa:physicalQuantity index="2">…<roa:basicData xsi:type="roa:TrafficSpeed"><roa:averageVehicleSpeed accuracy="0.0"> <com:speed>110.0</com:speed></roa:averageVehicleSpeed>… <roa:physicalQuantity index="4">…<com:speed>97.0</com:speed>… <roa:measurementTimeDefault><roa:timeValue>2026-10-16T21:40:00Z</roa:timeValue></roa:measurementTimeDefault>
- 3
Put the measuring points on the map
The location is not in the measurement file but in the configuration file (1.8 MB gzip, 147 MB XML): per point latitude and longitude, number of lanes, the carriageway type (mainCarriageway, exitSlipRoad …) and which index number belongs to which lane and vehicle class. Read it once a day; measuring points rarely change.
https://opendata.ndw.nu/snelheden_en_intensiteiten_configuratie_meetlocaties.xml.gz
<roa:measurementSite id="RWS01_MONIBAS_0010vwa0057ra" version="1742">… <roa:measurementSiteName><com:values><com:value lang="nl">0010vwa0057ra</com:value></com:values></roa:measurementSiteName> <roa:measurementSiteNumberOfLanes>1</roa:measurementSiteNumberOfLanes> …<roa:specificMeasurementValueType>trafficSpeed</roa:specificMeasurementValueType> …<loc:carriageway>exitSlipRoad</loc:carriageway>… <loc:latitude>52.34542</loc:latitude><loc:longitude>4.97708</loc:longitude>
- 4
Fetch jams, incidents and closures
actueel_beeld.xml.gz (DATEX II v3) holds everything happening on the road now, as situationRecords with a type: AbnormalTraffic (jam, with queueLength in metres and delayTimeValue in seconds), Accident, VehicleObstruction (stranded vehicle), RoadOrCarriagewayOrLaneManagement (closures and lane measures), SpeedManagement (temporary speed limit) and ReroutingManagement (diversion). The source is included: an RWS district, NDW or a province. There is no road number: the location is a line of coordinates (posList, latitude first, then longitude) and an Alert-C location code.
https://opendata.ndw.nu/actueel_beeld.xml.gz
<sit:situationRecord xsi:type="sit:AbnormalTraffic" id="NLRWS_0005489543_1" version="8">… <sit:source><com:sourceName><com:values><com:value lang="nl">NLRWS</com:value>… <com:overallStartTime>2026-10-16T18:57:25.772411Z</com:overallStartTime>… <sit:impact><sit:delays><sit:delayBand>upToTenMinutes</sit:delayBand><sit:delayTimeValue>322.0</sit:delayTimeValue></sit:delays></sit:impact> <sit:cause><sit:causeDescription><com:values><com:value lang="nl">Door grenscontrole</com:value>…<sit:causeType>other</sit:causeType></sit:cause> …<loc:posList>52.309761 7.010353 52.31377 7.043839</loc:posList>… <sit:abnormalTrafficType>stationaryTraffic</sit:abnormalTrafficType><sit:queueLength>2100</sit:queueLength></sit:situationRecord>
- 5
Look up the signs on site
NDW's traffic sign API (wegkenmerken) returns every sign with its RVV code. A1 is the speed limit; blackCode is the number on the sign. Never ask without a filter: all A1 signs in the Netherlands alone are 97 MB. A box (bbox=west,south,east,north in degrees) of about 4 by 4 km around a jam returns a few dozen signs. Check lastSeenOn: the signs along the A1 near Diemen were last seen in April 2022.
https://wegkenmerken.ndw.nu/api/rest/static-road-data/traffic-signs/v4/current-state?rvvCode=A1&roadAuthorityType=R&bbox=4.95,52.33,5.0,52.36
{"type":"FeatureCollection","features":[{"type":"Feature","id":"0278838e-1887-4283-af6f-a006af41593d", "geometry":{"type":"Point","coordinates":[4.9825913058197,52.342211534899]}, "properties":{"validated":"n","rvvCode":"A1","blackCode":"100","status":"PLACED","textSigns":[],"placement":"L", "side":"NW","bearing":323,"fraction":0.4749358296394348,"drivingDirection":"H","roadName":"Rijksweg A1", "roadType":1,"roadNumber":1,"roadSectionId":600125974,"nwbVersion":"2024-03-01","countyName":"Diemen", "countyCode":"GM0384","townName":"Diemen","imageUrl":"https://wegkenmerken.ndw.nu/api/images/c8760c1b-…", "firstSeenOn":"2014-09-02","lastSeenOn":"2022-04-20"}}, … - 6
Build the dashboard
Have a server fetch the two small files every minute (with If-Modified-Since), summarise them into a small JSON file and hand that to the page. On the map: measuring points as dots (green above 80, orange 50 to 80, red below 50 km/h), jams as red lines from posList, closures as grey lines. Next to the map the counters and a list of incidents with source and start time.
How the sources fit together
- NDW live traffic dataTraffic signs and accessibility map (NDW)via
coördinaten (bbox rond de file of het meetpunt)Jams and measuring points have coordinates but no limit. The traffic sign API returns the A1 signs with their speed inside a box, so you can see how far the measured speed drops below the limit.
- Traffic signs and accessibility map (NDW)National Road Database (NWB) and road datavia
roadSectionId = wegvak-id in het Nationaal Wegenbestand (let op nwbVersion)Each sign belongs to an NWB road section. That lets you draw the limit on the road network itself instead of as loose points.
Let AI build it
Paste this prompt into Claude, ChatGPT, Cursor or Lovable. It names the real endpoints, the join keys and the pitfalls, so the assistant writes working code straight away. Always check the result against the source itself.
Build a live traffic dashboard for the Netherlands: a Node 20 server that summarises open NDW files every minute, and one HTML page with a MapLibre or Leaflet map. Data (open, no key, DATEX II version 3, gzip): - Speeds and flows per minute: https://opendata.ndw.nu/snelheden_en_intensiteiten_meetgegevens.xml.gz (1 MB gzip, 70 MB XML) - Location of each measuring point (once a day): https://opendata.ndw.nu/snelheden_en_intensiteiten_configuratie_meetlocaties.xml.gz (1.8 MB gzip, 147 MB XML) - Jams, incidents, closures, diversions, temporary speed limits: https://opendata.ndw.nu/actueel_beeld.xml.gz (0.5 MB gzip) - Traffic signs (speed limit = RVV code A1, the number in blackCode): https://wegkenmerken.ndw.nu/api/rest/static-road-data/traffic-signs/v4/current-state?rvvCode=A1&bbox=west,south,east,north What you need to know: - Measurements: each siteMeasurements has a measurementSiteReference id and per lane alternately TrafficFlow (vehicleFlowRate, vehicles per hour) and TrafficSpeed (speed, km/h). speed -1 = no measurement. Average weighted by flow. Use a streaming XML parser (sax) or a regex per block; loading 70 MB into a DOM is slow. - RWS points are named RWS01_MONIBAS_0021hrl0403ra: road 002 (A2; numbers above 100 are N roads), then a 1, hrl/hrr = main carriageway left/right, 0403 = km 40.3. Use only main carriageway for 'speed on the motorway'. - Configuration: measurementSite id → latitude/longitude, carriageway, and per index the vehicleType and measurement type. Cache it for a day. - Live situation: situationRecord xsi:type = AbnormalTraffic (queueLength m, delayTimeValue s), Accident, VehicleObstruction, RoadOrCarriagewayOrLaneManagement (roadOrCarriagewayOrLaneManagementType: carriagewayClosures, laneClosures …), SpeedManagement (temporarySpeedLimit), ReroutingManagement. In force when overallStartTime ≤ now ≤ overallEndTime (the end can be missing). posList is 'lat lon lat lon …': swap for GeoJSON. There is no road number in the live situation file. - Traffic signs: always filter (without a filter tens of MB), unknown parameters are silently ignored. Show lastSeenOn; many signs have not been seen again for years. - NDW stops the old v2.3 files (trafficspeed.xml.gz etc.) on 9 February 2027; don't use them any more. - Fetch the files at most once a minute, with your own User-Agent and If-Modified-Since. Output: counters at the top (measuring points, below 50 km/h, jams, accidents, stranded vehicles, closures now), a map with coloured measuring points and jams as lines, a table per motorway with the average speed, and a list of incidents with source, start time, jam length, delay and the limit according to nearby signs.
Try it yourself
The core of the project in a small script. Needs: Node 20+ (fetch en zlib ingebouwd), geen pakketten. Tested on 2026-10-16; below is the real output of that run.
// Files en wegwerkzaamheden nu: snelheden op de snelweg, incidenten en afsluitingen, plus de borden ter plaatse.
// Bronnen: NDW open data (DATEX II v3) en de NDW-verkeersborden-API. Node 20+, geen pakketten nodig.
import zlib from "node:zlib";
const UA = { "User-Agent": "mijn-filedashboard/1.0 (+jij@voorbeeld.nl)" };
const ndw = async (bestand) => { const r = await fetch("https://opendata.ndw.nu/" + bestand, { headers: UA });
if (!r.ok) throw new Error(`${bestand}: HTTP ${r.status}`); return zlib.gunzipSync(Buffer.from(await r.arrayBuffer())).toString("utf8"); };
const tag = (xml, naam) => xml.match(new RegExp(`<[a-z]+:${naam}>([^<]*)<`))?.[1];
// 1. Snelheden per minuut: 1 MB gzip, 70 MB XML. Alleen RWS-meetpunten op de hoofdrijbaan.
// Naam 0021hrl0403ra: weg 002 (A2), dan altijd een 1, hrl/hrr = hoofdrijbaan links/rechts, 0403 = km 40,3.
const metingen = await ndw("snelheden_en_intensiteiten_meetgegevens.xml.gz");
const meettijd = tag(metingen, "publicationTime").slice(0, 16);
const perWeg = {}; let punten = 0, traag = [];
for (const blok of metingen.split("<roa:siteMeasurements>").slice(1)) {
const id = blok.match(/id="([^"]+)"/)[1], m = id.match(/^RWS01_MONIBAS_(\d{4})hr[lr](\d{4})/); if (!m) continue;
let stroom = 0, som = 0, n = 0; // per rijstrook: eerst intensiteit (voertuigen/uur), dan snelheid (km/u); -1 = geen meting
for (const [, soort, waarde] of blok.matchAll(/xsi:type="roa:(TrafficFlow|TrafficSpeed)">.*?<com:(?:vehicleFlowRate|speed)>([-\d.]+)</g)) {
if (soort === "TrafficFlow") stroom = Number(waarde); else if (Number(waarde) > 0 && stroom > 0) { som += Number(waarde) * stroom; n += stroom; } }
if (!n) continue;
const nr = Number(m[1].slice(0, 3)), weg = (nr < 100 ? "A" : "N") + nr, v = som / n; punten++;
(perWeg[weg] ??= []).push(v);
if (v < 50) traag.push({ weg, km: Number(m[2]) / 10, v: Math.floor(v) });
}
// 2. Actueel beeld: incidenten, afsluitingen, wegwerkzaamheden en tijdelijke maximumsnelheden (0,5 MB gzip)
const beeld = await ndw("actueel_beeld.xml.gz"), nu = new Date();
const records = [...beeld.matchAll(/<sit:situationRecord xsi:type="sit:(\w+)"[\s\S]*?<\/sit:situationRecord>/g)].map(([xml, soort]) => ({
soort, oorzaak: tag(xml, "causeType"), maatregel: tag(xml, "roadOrCarriagewayOrLaneManagementType"), start: tag(xml, "overallStartTime"),
eind: tag(xml, "overallEndTime"), bron: xml.match(/<com:sourceName><com:values><com:value lang="nl">([^<]*)/)?.[1],
file: tag(xml, "queueLength"), vertraging: tag(xml, "delayTimeValue"), punt: tag(xml, "posList")?.split(" ").slice(0, 2).map(Number) })); // let op: eerst breedte, dan lengte
const actief = records.filter((r) => new Date(r.start) <= nu && (!r.eind || new Date(r.eind) >= nu));
const tel = (lijst, f) => Object.entries(lijst.reduce((t, r) => ((t[f(r)] = (t[f(r)] ?? 0) + 1), t), {})).sort((a, b) => b[1] - a[1]);
console.log(`Snelheden ${meettijd}: ${punten} RWS-meetpunten op de hoofdrijbaan met verkeer, ${traag.length} onder 50 km/u`);
for (const [weg, vs] of Object.entries(perWeg).sort((a, b) => b[1].length - a[1].length).slice(0, 4))
console.log(` ${weg}: ${vs.length} meetpunten, gemiddeld ${Math.round(vs.reduce((s, v) => s + v, 0) / vs.length)} km/u`);
for (const t of traag.slice(0, 5)) console.log(` traag: ${t.weg} bij km ${t.km}: ${t.v} km/u`);
console.log(`\nActueel beeld: ${records.length} meldingen, ${actief.length} nu geldig`);
console.log(" nu geldig per soort:", tel(actief, (r) => r.soort).map(([k, n]) => `${k} ${n}`).join(", "));
console.log(" afsluitingen:", tel(actief.filter((r) => r.maatregel), (r) => r.maatregel).slice(0, 4).map(([k, n]) => `${k} ${n}`).join(", "));
console.log(" meeste meldingen van:", tel(actief, (r) => r.bron).slice(0, 3).map(([k, n]) => `${k} (${n})`).join(", "));
const files = actief.filter((r) => r.soort === "AbnormalTraffic");
for (const f of files) console.log(` file: ${f.file ?? "?"} m, vertraging ${Math.round((f.vertraging ?? 0) / 60)} min, bij ${f.punt.join(", ")}`);
// 3. Welke snelheid geldt daar volgens de borden? (NDW-verkeersborden, A1 = maximumsnelheid, kleine bbox rond de file)
if (files.length) { const [lat, lon] = files[0].punt, d = 0.02;
const borden = (await (await fetch(`https://wegkenmerken.ndw.nu/api/rest/static-road-data/traffic-signs/v4/current-state?rvvCode=A1&bbox=${lon - d},${lat - d},${lon + d},${lat + d}`, { headers: UA })).json()).features;
console.log(` borden A1 binnen ~2 km: ${borden.length};`, tel(borden, (b) => `${b.properties.roadName} ${b.properties.blackCode} km/u`).slice(0, 3).map(([k, n]) => `${k} (${n}x)`).join(", ")); }
Snelheden 2026-10-16T21:46: 6264 RWS-meetpunten op de hoofdrijbaan met verkeer, 1 onder 50 km/u A2: 820 meetpunten, gemiddeld 107 km/u A12: 596 meetpunten, gemiddeld 109 km/u A1: 542 meetpunten, gemiddeld 111 km/u A15: 428 meetpunten, gemiddeld 108 km/u traag: A2 bij km 251.1: 10 km/u Actueel beeld: 1508 meldingen, 1498 nu geldig nu geldig per soort: RoadOrCarriagewayOrLaneManagement 1069, ReroutingManagement 157, SpeedManagement 147, VehicleObstruction 113, GeneralNetworkManagement 4, GeneralObstruction 4, Accident 2, AbnormalTraffic 2 afsluitingen: carriagewayClosures 734, laneClosures 161, other 80, lanesDeviated 50 meeste meldingen van: WNZ-Z [RWS West-Nederland Zuid District Zuid] (133), ZN-W [RWS Zuid-Nederland District West] (112), ZN-M [RWS Zuid-Nederland District Midden] (91) file: 2100 m, vertraging 5 min, bij 52.309761, 7.010353 file: 2100 m, vertraging 7 min, bij 51.93462, 4.153484 borden A1 binnen ~2 km: 29; Bentheimerstraat 60 km/u (6x), Rijksweg A1 100 km/u (4x), Ambachtstraat 30 km/u (3x)
Where it goes wrong
- Small file, big memory. The measurements are 1 MB gzip but 70 MB XML, the configuration 147 MB. A DOM parser in the browser chokes on that. Process on the server and send only a summary.
- Speed -1 is not a speed. Without traffic (or with a faulty loop) it says -1. Count that and you see jams everywhere at night. Weight by flow and skip lanes without vehicles.
- The road number is hidden. 0021hrl… is the A2, not the A21: the fourth digit is always a 1. Numbers above 100 (2051, 3251) are N roads managed by Rijkswaterstaat.
- Latitude and longitude swapped. posList has latitude first (52.31) and then longitude (7.01). GeoJSON wants it the other way round; otherwise your jams end up in the Indian Ocean.
- Old files are stopping. trafficspeed.xml.gz and measurement.xml.gz (DATEX II v2.3) disappear on 9 February 2027. On 16 October measurement.xml.gz had not been updated since 12 October. Build on the v3 files straight away.
- Signs are not always current. All 32 signs in the test (A1, near Diemen) had validated = n and a lastSeenOn from 2022. A temporary limit (SpeedManagement in the live situation) or a matrix sign overrides the fixed sign.
Take it further
- Rush hour visualised: store the average speed per motorway every minute and draw a heatmap of hour against weekday.
- Effect of roadworks: compare the speed on and around a closed stretch in the week before and during the closure.
- Matrix signs: Matrixsignaalinformatie.xml.gz (1 MB) gives every minute what the matrix signs above the motorway show (speed, cross, arrow). Put them next to the measured speed.
- Add air quality: link the flow at a measuring point to the NO2 measurements of a Luchtmeetnet station along the same road, per hour.
The sources2

NDW live traffic data
Speeds and volumes from tens of thousands of road sensors, refreshed every minute. Also roadworks and incidents.

Traffic signs and accessibility map (NDW)
The National Road Traffic Data Portal keeps a national file of traffic signs: per sign the RVV code (A1 for a speed limit, C22a for a low-emission zone …), additional panels, side of the road, the NWB road section, municipality, traffic order and since when the sign has been seen. Amsterdam alone had 97,175 signs on 16 October 2026. The traffic sign API v4 returns the current state (updated daily) and all changes; the whole file can also be downloaded. Plus an accessibility map that computes per municipality which roads are open to a vehicle of given dimensions, still in beta.
Train delay and disruption dashboard
How fast is your municipality going electric?
Charging points per neighbourhood and per electric car
Live map of buses, trams and metros
What is flying over my house right now?