Monte Carlo forecasting answers the question that comes up in almost every planning meeting sooner or later: “When will it be done?” And it does so without an estimation session, without story points and without anyone pulling a number out of thin air that they will be measured against later. All you need is a list of the days on which your team finished tickets over the past few weeks.
This topic keeps me busy because I often see how much energy goes into estimates and how little of it ends up in the actual date. After my article on WIP limits, the obvious follow-up question was: if work flows more evenly, can I derive delivery dates from it? Yes, you can. In this article you will build a small TypeScript script for exactly that, one you can run in any retro or review.
In short
A Monte Carlo simulation plays through your team’s future thousands of times by randomly drawing days from your real past. The result is not a single date but a range with probabilities, such as “85 percent confidence by December 18”. The most common mistake is leaving out days on which no ticket was finished, which makes the forecast far too optimistic.
What you will learn in this article
- How Monte Carlo forecasting with throughput data works
- How to answer “When?” and “How many?” correctly
- How to build a runnable TypeScript script with tests yourself
- How to communicate percentages so that stakeholders understand them
Why estimates rarely produce a delivery date
Story points are a tool for talking about the size and risk of a task as a team. They are good at that. Problems start when the sum of the points is supposed to become a delivery date. Suddenly three assumptions are hidden in the calculation: that the estimate is right, that velocity stays stable and that nothing gets in the way. Each one of them is shaky.
On top of that, the classic formula “remaining points divided by velocity” ignores something crucial: waiting time. A two-point ticket can sit for three weeks because it is waiting for a review, an approval or another team. That does not show up in the estimate, but it does show up in the calendar. A team’s throughput, by contrast, already contains all of this, because it only counts what actually got finished.
That is why I flip the question. Instead of “How big is the work?” I ask “How much has this team actually delivered under real conditions so far?” and let a forecast be calculated from that. It is more honest, faster and spares the team quite a few estimation sessions.
What is a Monte Carlo simulation?
A Monte Carlo simulation is a computational method that does not calculate a result exactly but approximates it through a large number of random runs. The idea dates back to the 1940s: mathematician Stanisław Ulam developed it at Los Alamos, and the name refers to the casino in Monaco. Today the method is used in weather models, insurance risk calculations and, yes, project planning.
One picture is enough for our purposes. Imagine writing down on a slip of paper, for every workday of the last six weeks, how many tickets were finished that day. All slips go into a hat. Now you draw a slip, note the number, put it back and draw again, until the sum reaches the number of your open tickets. The number of draws is one possible future, measured in workdays. You repeat the whole thing ten thousand times. In the end you do not have one number but a distribution: some runs finish after 15 days, some need 40, and most land somewhere in between.
Throughput: the one number you need
The Kanban Guide names four flow metrics, and one of them is throughput: the number of work items finished per unit of time. It is a simple count, not points and not hours. That is exactly what makes it so robust. Whether a ticket was big or small evens out over many days, as long as you slice your tickets to a reasonably similar size.
You probably already have the data. Every ticket system can export when a ticket moved into the “Done” column. The simulation needs nothing more.
How Monte Carlo forecasting works for delivery dates
The animation shows a single run. A day is drawn at random from the history and its throughput moves into the simulation. As soon as the sum reaches the open tickets, the result lands in the distribution.
🎬 How a Monte Carlo simulation with throughput data works – this animated diagram only runs on my site: watch it on sascha-ms.de
Two questions: when and how many?
In practice you will meet two variants of the same question. The first one is: “When will these 25 tickets be done?” Here the scope is fixed and you are looking for a date. The second one is: “How much can we get done by the release in two weeks?” Here the date is fixed and you are looking for the scope. Both can be simulated with the same data, only the loop looks slightly different.
One detail is often overlooked. For the question “When?”, more confidence means a later date, so you read the 85th percentile of the simulated days. For the question “How many?”, however, more confidence means fewer tickets. If you want to say with 85 percent confidence “we will finish at least this many”, you need the 15th percentile. Simply using the same number here promises exactly the wrong thing.
Monte Carlo forecasting in TypeScript, step by step
Now let’s get practical. As an example I use a fictional team building an app for a bike rental shop. It has finished 33 tickets over the last six weeks and wants to know when the 25 tickets of the next feature will be done.
Prerequisites
- Node.js 22 or newer
- A CSV export from your ticket system with ticket ID and finish date
- Basic TypeScript knowledge; I explain the statistics along the way
Step 1: Set up the project
The script has no runtime dependencies. For development I use tsx to run TypeScript directly and Vitest for the tests.
mkdir monte-carlo-forecast && cd monte-carlo-forecast
npm init -y
npm install --save-dev typescript tsx vitest @types/node
mkdir src test data
Then replace the package.json with this version and create the tsconfig.json. The important part is allowImportingTsExtensions, so that imports ending in .ts work for both tsx and the type check.
{
"name": "monte-carlo-forecast",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"forecast": "tsx src/cli.ts",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.10.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"types": ["node"],
"skipLibCheck": true
},
"include": ["src", "test"]
}
Step 2: Export your throughput data
Export all tickets that were finished in the chosen period and save them as data/finished.csv. One line per ticket is enough:
ticket,finished
RAD-101,2026-09-28
RAD-102,2026-09-29
RAD-103,2026-09-29
...
Only include tickets of a similar type, such as stories and bugs, but no epics or subtasks. And make sure the finish date really means “done”, meaning shipped or accepted, not just “code written”.
Step 3: Write the simulation
The core consists of five small functions. dailyThroughput turns the finish dates into a list of throughput per workday, zeros included. simulateWhen and simulateHowMany are the two simulations, percentile reads the results, and addWorkdays turns workdays into a calendar date.
A word about randomness: instead of Math.random() I use a small random generator with a fixed starting value (seed). This makes the result identical on every run, which makes tests and comparisons in the retro much easier.
// src/monte-carlo.ts
// Monte Carlo forecast from throughput data: no dependencies, just Node.js.
/** Deterministic random generator (mulberry32) so tests are reproducible. */
export function createRandom(seed: number): () => number {
let state = seed >>> 0;
return () => {
state = (state + 0x6d2b79f5) >>> 0;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const isWorkday = (d: Date): boolean => d.getUTCDay() !== 0 && d.getUTCDay() !== 6;
const toDay = (d: Date): string => d.toISOString().slice(0, 10);
/**
* Turns a list of finish dates (YYYY-MM-DD) into throughput per workday.
* Important: days without a finished ticket count as 0 and must be included.
*/
export function dailyThroughput(finishedDates: string[], from: string, to: string): number[] {
const counts = new Map<string, number>();
for (const date of finishedDates) counts.set(date, (counts.get(date) ?? 0) + 1);
const samples: number[] = [];
for (let d = new Date(`${from}T00:00:00Z`); toDay(d) <= to; d.setUTCDate(d.getUTCDate() + 1)) {
if (isWorkday(d)) samples.push(counts.get(toDay(d)) ?? 0);
}
if (samples.length === 0) throw new Error('No workdays in the selected period');
if (samples.every((s) => s === 0)) throw new Error('Nothing was finished in this period – no forecast possible');
return samples;
}
/** Value that p percent of the sorted results do not exceed (nearest-rank method). */
export function percentile(sorted: number[], p: number): number {
const rank = Math.ceil((p / 100) * sorted.length);
return sorted[Math.min(sorted.length, Math.max(1, rank)) - 1];
}
export interface ForecastOptions {
trials?: number;
seed?: number;
}
/** Question 1: “When will N tickets be done?” – returns the workdays needed per run (sorted). */
export function simulateWhen(samples: number[], remainingItems: number, opts: ForecastOptions = {}): number[] {
const { trials = 10_000, seed = 42 } = opts;
if (remainingItems <= 0) throw new Error('remainingItems must be greater than 0');
const random = createRandom(seed);
const results: number[] = [];
for (let i = 0; i < trials; i++) {
let done = 0;
let days = 0;
while (done < remainingItems) {
done += samples[Math.floor(random() * samples.length)];
days++;
}
results.push(days);
}
return results.sort((a, b) => a - b);
}
/** Question 2: “How many tickets can we finish in X workdays?” – returns the count per run (sorted). */
export function simulateHowMany(samples: number[], workdays: number, opts: ForecastOptions = {}): number[] {
const { trials = 10_000, seed = 42 } = opts;
const random = createRandom(seed);
const results: number[] = [];
for (let i = 0; i < trials; i++) {
let done = 0;
for (let day = 0; day < workdays; day++) {
done += samples[Math.floor(random() * samples.length)];
}
results.push(done);
}
return results.sort((a, b) => a - b);
}
/** Date of the n-th workday from start (if the start is a workday, it is day 1). */
export function addWorkdays(start: string, workdays: number): string {
const d = new Date(`${start}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() - 1);
let left = workdays;
while (left > 0) {
d.setUTCDate(d.getUTCDate() + 1);
if (isWorkday(d)) left--;
}
return toDay(d);
}
Step 4: Build the command line
The CLI reads the CSV file, calculates the samples and prints both forecasts for 50, 70, 85 and 95 percent. Arguments are handled by parseArgs from the Node standard module node:util, so you do not need any extra library.
// src/cli.ts
import { readFileSync } from 'node:fs';
import { parseArgs } from 'node:util';
import { addWorkdays, dailyThroughput, percentile, simulateHowMany, simulateWhen } from './monte-carlo.ts';
const { values } = parseArgs({
options: {
file: { type: 'string', default: 'data/finished.csv' },
from: { type: 'string' },
to: { type: 'string' },
remaining: { type: 'string', default: '25' },
start: { type: 'string' },
workdays: { type: 'string', default: '10' },
},
});
// CSV with header "ticket,finished" – one line per finished ticket
const finished = readFileSync(values.file!, 'utf8')
.trim()
.split('\n')
.slice(1)
.map((line) => line.split(',')[1].trim());
const from = values.from ?? [...finished].sort()[0];
const to = values.to ?? [...finished].sort().at(-1)!;
const start = values.start ?? to;
const samples = dailyThroughput(finished, from, to);
const remaining = Number(values.remaining);
const workdays = Number(values.workdays);
console.log(`History: ${from} to ${to}, ${samples.length} workdays, ${finished.length} tickets finished`);
const when = simulateWhen(samples, remaining);
console.log(`\nWhen will ${remaining} tickets be done? (start: ${start})`);
for (const p of [50, 70, 85, 95]) {
const days = percentile(when, p);
console.log(` ${p}% confidence: by ${addWorkdays(start, days)} (${days} workdays)`);
}
const howMany = simulateHowMany(samples, workdays);
console.log(`\nHow many tickets in ${workdays} workdays?`);
for (const p of [50, 70, 85, 95]) {
// “At least X with p% confidence” = the (100 - p)th percentile
console.log(` ${p}% confidence: at least ${percentile(howMany, 100 - p)} tickets`);
}
Step 5: Write tests
Many people shy away from testing randomness because the results change. With a fixed seed and a few special cases it works well anyway. With constant throughput, for example, the simulation has to be exact, and higher confidence must never produce an earlier date.
// test/monte-carlo.test.ts
import { describe, expect, it } from 'vitest';
import { addWorkdays, dailyThroughput, percentile, simulateHowMany, simulateWhen } from '../src/monte-carlo.ts';
describe('dailyThroughput', () => {
it('counts workdays without a finished ticket as 0', () => {
// 2026-11-02 is a Monday, 2026-11-07/08 are the weekend
const samples = dailyThroughput(['2026-11-02', '2026-11-02', '2026-11-04'], '2026-11-02', '2026-11-08');
expect(samples).toEqual([2, 0, 1, 0, 0]);
});
it('throws when nothing was finished in the period', () => {
expect(() => dailyThroughput([], '2026-11-02', '2026-11-06')).toThrow(/no forecast/);
});
});
describe('percentile', () => {
it('returns the nearest-rank value', () => {
const sorted = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
expect(percentile(sorted, 50)).toBe(5);
expect(percentile(sorted, 85)).toBe(9);
expect(percentile(sorted, 100)).toBe(10);
});
});
describe('simulateWhen', () => {
it('is exact with constant throughput', () => {
const result = simulateWhen([2], 10, { trials: 100 });
expect(new Set(result)).toEqual(new Set([5]));
});
it('is reproducible with the same seed', () => {
const samples = [0, 1, 3, 0, 2];
expect(simulateWhen(samples, 20, { seed: 7 })).toEqual(simulateWhen(samples, 20, { seed: 7 }));
});
it('never returns an earlier date for higher confidence', () => {
const result = simulateWhen([0, 1, 2, 0, 3, 1], 25);
expect(percentile(result, 85)).toBeGreaterThanOrEqual(percentile(result, 50));
});
});
describe('simulateHowMany', () => {
it('returns exactly days times throughput with constant throughput', () => {
expect(new Set(simulateHowMany([3], 4, { trials: 50 }))).toEqual(new Set([12]));
});
});
describe('addWorkdays', () => {
it('counts the start day and skips the weekend', () => {
// Friday is day 1, Monday is day 2
expect(addWorkdays('2026-11-06', 1)).toBe('2026-11-06');
expect(addWorkdays('2026-11-06', 2)).toBe('2026-11-09');
});
});
npm run typecheck
npm test
Expected result: the type check finishes without output and Vitest reports eight passing tests.
Step 6: Run the forecast
Specify the history period explicitly. Otherwise it ends at the last finish date, and a workday without any finished ticket at the end would be dropped.
npm run forecast -- --from 2026-09-28 --to 2026-11-06 --start 2026-11-09 --remaining 25 --workdays 10
History: 2026-09-28 to 2026-11-06, 30 workdays, 33 tickets finished
When will 25 tickets be done? (start: 2026-11-09)
50% confidence: by 2026-12-09 (23 workdays)
70% confidence: by 2026-12-14 (26 workdays)
85% confidence: by 2026-12-18 (30 workdays)
95% confidence: by 2026-12-24 (34 workdays)
How many tickets in 10 workdays?
50% confidence: at least 11 tickets
70% confidence: at least 8 tickets
85% confidence: at least 6 tickets
95% confidence: at least 4 tickets
Here is how to read it: on average the team finished about 1.1 tickets per workday. In half of the simulated futures the 25 tickets are done by December 9. If you want 85 percent confidence, plan for December 18. The histogram shows how wide the range is.
Distribution of 10,000 simulated runs: the coloured bars are within the 85 percent line.
How to communicate a forecast
The best simulation is of little use if only “December 18” reaches the steering committee. That is why the probability always belongs next to the date. A sentence that works well: “Based on our data from the last six weeks, we are 85 percent likely to be done by December 18. We recalculate every week.”
Why 85 percent?
The 85 percent is not a law of nature but a convention that has become common in the Kanban community. The Kanban Guide, for instance, uses it in its example of a service level expectation. I like it because it is honest: in roughly one out of seven cases, it will take longer. That is rare enough to be reliable and frequent enough not to give anyone a false guarantee. For a trade fair date that cannot be moved, on the other hand, I use 95 percent.
Recalculate regularly
A forecast is a snapshot. If new tickets are added, someone gets sick or the order changes, the result shifts as well. The big advantage over a one-off estimate: recalculating takes seconds. I would update the forecast once a week, for example right before the review, and make the trend of the dates visible. If the 85 percent date keeps moving back over several weeks, that is an early warning sign you should talk about.
Common mistakes and how to avoid them
Leaving out the zeros. If you only count days on which something was finished, you are simulating a team that never has a bad day. In our example, there was not a single finished ticket on 13 out of 30 workdays. Without these days, the forecast would be weeks too early.
Data that is too old. If the team has changed or you have changed your process, data from six months ago describes a different system. I like to use the last six to eight weeks and check whether an outlier such as a holiday month is included.
Ignoring growing scope. In my experience, 25 tickets rarely stay exactly 25. Track how many tickets were added to past features and work with a range instead, for example once with 25 and once with 32 tickets.
Forgetting public holidays. The script only knows weekends. If a date falls into the Christmas season like in the example, add days off and vacations by hand or extend isWorkday with a list of holidays.
Selling the 50 percent value as a commitment. The median looks temptingly early. But it means you will be late in every second case. It is not fit to be a commitment.
My take
I consider Monte Carlo forecasting one of the most underrated tools in agile teams. It takes pressure off the team, because nobody has to answer for an estimate they gave under uncertainty. At the same time, it gives stakeholders something they never get from story points: an honest statement about risk.
Where I disagree: the simulation does not replace conversations about the size and slicing of tickets. It assumes that your tickets are of roughly comparable size. If a monster ticket sits next to many small tasks, every forecast gets shaky. The question “Can we slice this smaller?” therefore remains valuable, only the goal is different: flow instead of a sum of points. And together with WIP limits, throughput becomes more stable, which in turn makes the forecast more accurate.
One last thought as a leader: numbers like throughput are easily misused to measure performance. Please do not use them to compare teams or individuals. As soon as that happens, tickets get sliced smaller just to make the number go up, and the forecast loses its value. You can read more about using metrics without turning them into a control tool in my article on metrics for AI-generated code.
FAQ
How much data do I need for Monte Carlo forecasting?
Less than many people think. Throughput data from a few weeks is already enough for useful forecasts. More important than the amount is that the data matches the current team and process.
Does Monte Carlo forecasting work with Scrum?
Yes. Instead of days, you can also use finished tickets per sprint. With daily data, however, you have more samples and notice changes sooner.
Do I still need story points?
Not for the forecast. Many teams keep a short conversation about a ticket’s size to check whether it is sliced small enough. Assigning points is not necessary for that.
How many runs make sense?
Beyond a few thousand runs, the percentiles hardly change. The script uses 10,000, which finishes in milliseconds on any laptop.
Your next step
Export the finish dates of the last six weeks from your ticket system and run the script once against your current feature. Compare the 85 percent date with the date you already have in mind, and bring both to your next retro. You will find more guides on teams and collaboration in the blog.
Now it is your turn: how does your team answer the question “When will it be done?” With estimates, with data or with gut feeling? Let me know in the comments.
All code from this article, ready to run: smirowstanitzok/sascha-ms-examples
Sources
- The Kanban Guide – definitions of throughput, cycle time and service level expectation
- Colleen Johnson: Forecasting Through Epic Uncertainty (ProKanban.org, 2026)
- Daniel Vacanti: When Will It Be Done? (book)
- Focused Objective: forecasting spreadsheets (Throughput Forecaster)
- Wikipedia: Monte Carlo method (history and basic idea)
- Node.js documentation: util.parseArgs
This post was created with AI assistance.
👉 Next step: More for team leads: my cheat sheets put the essentials on one printable page.
📬 Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).
Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)