Search "average typing speed" and you'll get confident numbers — 40 WPM, 65 WPM, "professionals hit 75" — almost always with no passage, no method, and no dataset behind them. I wanted one I could actually defend, so I built a live typing test that collects an open, anonymized dataset and, crucially, refuses to publish an average until it has enough data to mean anything.
It runs on Cloudflare Workers + D1. It's live, the dataset is downloadable, and every headline number is reproducible from a script. Here's how it's built and the decisions that keep it honest.
Why WPM is hard to measure fairly
Three problems sink most "typing speed" numbers:
- Comparability. If everyone types different text, WPM isn't comparable — a familiar sentence types faster than a technical one. Fix: one fixed, standardized passage for everyone.
- Gross vs. net. Counting every keystroke rewards fast-and-sloppy. Fix: net WPM — only characters that match the passage at the same position count, so mistakes lower your score.
- Cheating. A paste "types" 300 WPM. Fix: block paste, and reject anything outside a plausible human range server-side.
The scoring is deliberately boring and standard:
// a character counts only if it matches the passage at that position
let correct = 0;
for (let i = 0; i < Math.min(typed.length, passage.length); i++) {
if (typed[i] === passage[i]) correct++;
}
const netWpm = (correct / 5) / (elapsedMs / 60000); // 5 chars = 1 "word"
const accuracy = correct / typed.length; // as a percentage
Enter fullscreen mode Exit fullscreen mode
Keeping submissions honest (without accounts)
No login, but a few cheap gates keep the dataset clean:
- A short-lived HMAC token fetched right before submit — blocks tokenless
curlspam. - A 1–250 WPM plausibility bound — a paste or bot lands well above 250 and is dropped.
- A per-visitor daily cap, so one source can't skew the set.
- Privacy: the server never stores your IP. It keeps a salted SHA-256 hash purely for rate-limiting, and the public export contains only
timestamp, wpm, accuracy.
The part I care about most: the honesty gate
With a few dozen samples, one fat-fingered run swings the average wildly. So the page hides the headline until it has 1,000 results, and reports the median (robust to outliers) rather than the mean:
function renderHeadline(d) {
if (!d || d.count < 1000) {
return `Collecting data — ${d ? d.count : 0} tests so far.`;
}
return `The median typist reaches ${Math.round(d.median_wpm)} net WPM ` +
`at ${Math.round(d.median_accuracy)}% accuracy — over ${d.count} tests.`;
}
Enter fullscreen mode Exit fullscreen mode
Right now it genuinely says "collecting data — 0 tests." That's the point: I'd rather show an honest zero than a made-up average. Typing speed is a trained skill, so the honest description isn't a single number anyway — it's a distribution, which the page draws as a live histogram once results come in.
It's reproducible
The stats aren't a black box. There's a public CSV export and a small Node harness that recomputes every published figure — median WPM, median accuracy, trimmed mean, percentiles, histogram — using a verbatim copy of the site's own stats code:
node typing-speed-study.mjs
# fetches https://lkforge.com/api/typing/export and recomputes the numbers
Enter fullscreen mode Exit fullscreen mode
So the claim isn't "trust my dashboard," it's "here's the raw data and the exact math — run it yourself."
Try it (and feed the dataset)
The whole thing only becomes interesting with data, and it's at zero. If you want to add a point and watch the distribution build:
- Take the test: https://lkforge.com/tools/typingtrack/typing-speed-study/
- Reproduce the numbers: https://gist.github.com/lucian-devops/063798164ec86d076e977d003eb87c30
- Raw dataset (CSV, CC BY 4.0): https://lkforge.com/api/typing/export
I'll write a follow-up with the actual distribution once there's enough data to report one honestly. Curious what people think of the honesty-gate approach — worth the "boring zero," or would you just show the running average with a caveat?
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.