About the project
Where these numbers come from
This site is a fast, explorable front end for the DUV ultramarathon archive. It adds no results of its own — everything here is read from a public source, checked on the way in, and re-checked as races get corrected. This page explains that pipeline, and where it has limits.
01 · Source
Where the data comes from
Every event, result and runner on this site comes from the Deutsche Ultramarathon-Vereinigung and its public statistics archive at statistik.d-u-v.org (opens in a new tab) — the largest openly available collection of ultramarathon results anywhere, assembled and maintained over decades by its volunteers.
This project is a reader of that archive, not a second source. Every record it holds comes from DUV, apart from a small number created here and marked as such, and where the two disagree, DUV is right by definition.
02 · Scale
What is in it
- Race events
- 119,446
- Results
- 10,509,326
- Runners
- 2,521,586
These are read from the database on every page load — whatever the archive holds right now, not a snapshot written into this page.
03 · Pipeline
How the data is collected
A scraper reads the public DUV pages and writes what it finds into this project's database. It runs nightly, and it re-reads recent events for a period after they take place, because results get corrected in the days and weeks after a race — a provisional finish list is not a final one.
Writes are idempotent: running the scraper twice cannot duplicate a record, and a record that has been corrected by hand is never overwritten by a later run.
04 · Limits
What the archive does and does not say
Fifty years of race reporting is not uniform, and some fields mean different things in different races. These are corrections applied as data comes in — not errors at the source. A field holding a lap count is correct on a backyard ultra; it only becomes a problem when a reader assumes it is a speed.
- Backyard ultras record laps, not speed.In a backyard race the average-speed field holds the number of laps a runner completed. This is detected per event rather than per result, and those finishes are kept out of pace charts while still counting everywhere else.
- Some results belong to no identified runner.The source pools these under a single catch-all record. It is kept out of the runner list but stays reachable, and it explains itself — if you have landed on a runner whose results span more than a century, that is what you are looking at.
- Some names are placeholders.Where a finisher’s name was not recorded, the source writes a stand-in. These are identified by their structure rather than by matching a list of known spellings, and they are never rendered as a person’s name.
- Age-graded performance needs a year of birth.Where one is missing, the age-graded field simply repeats the raw result. Rather than draw a second line identical to the first, the comparison is left off — which is why some runner pages show one performance line and others show two.
05 · Build
How it is built
- Frontend
- React and TanStack Start, server-rendered
- API
- NestJS
- Database
- MongoDB
- Scraper
- TypeScript
- Components
- phanes-ui — built for this project and published to npm as a standalone package
- Charts
- Hand-built SVG. No charting library.
06 · Project
About this project
A bachelor thesis project, built to find out what falls out of a large, messy, real-world sports dataset when you try to make it fast and honest to browse.
The interesting problems turned out not to be the charts. They were the ones in the section above.