On the morning of 28 September, 37 Dependabot alerts appeared in my blog repository's security tab. By evening every one of them was closed. GitHub's own dashboard showed my mean time to remediate as 8.4 hours. The severity labels weren't mild either: one critical, 24 high, 12 medium. You could put that number on a CV.
A month later I went back and looked at the advisory publication dates. The oldest one was already 132 days old by the time its alert existed.
8.4 hours isn't wrong. It simply counts from a starting line I drew myself.
Where the clock starts
GitHub's definition of "mean time to remediate" is spelled out in the docs: it measures from the date the alert was created to the date it closed. In the documentation's own words, the metric is "the average age of all alerts that were remediated or dismissed" during the selected period. The formula sits on the same page: subtract the alert's creation date from the date it was last closed.
So when does an alert get created? The documentation's own wording matters here. Under the heading for when Dependabot sends alerts, two situations are listed: a new advisory being published, and your dependency graph changing. The third situation isn't on that list — it sits under a different heading, with a different verb: "GitHub immediately generates the dependency graph and creates alerts". Creates, not sends.
So switch the lamp on and everything already in the room gets an alert. How long those things had been sitting there is not written into the alert's date. That verb difference isn't innocent either — as we'll see in a moment, it is exactly what does and doesn't reach your inbox.
All 37 of my alerts were born on 28 September between 07:39:16 and 07:39:29 UTC, spread across thirteen seconds. There was no alert record in the repository before that day; the numbering starts at 1. The same day, the repository moved from a personal account to the organization.
In honesty I can't separate the two from the record: I may have switched scanning on that morning, or the transfer itself may have kicked off the first scan. Both were my doing, and either way the result is the same — the clock started that morning and counted nothing before it.
The docs mention one more thing, somewhere easy to miss: when Dependabot is first enabled, GitHub "does not send notifications for all vulnerable dependencies" it finds. The alerts are created; the mail doesn't arrive. The moment you most need a notification is the moment there isn't one.
Measuring the real ages
Counting from the advisory's publication date instead of the alert's birthday looks straightforward, but there's a trap: on the day an advisory was published, that package may not have been in my lockfile at all. Claiming "I was exposed for 132 days" over a dependency I added later would be fabrication.
So I checked all 37 against one question: on the day the advisory was published, was the version present in that lockfile — as it stood on that date — inside the advisory's vulnerable version range? The measurement walked both manifests separately (package-lock.json and remotion/package-lock.json for the video shorts), read each file's latest state in git history as of each advisory date, and compared the version against the range.
The answer: 37 out of 37. In every case, the vulnerable version really was in the repository on the day the advisory went public.
| Measurement | Value |
|---|---|
| Alerts | 37 (34 distinct advisories, 16 distinct packages) |
| Advisory → alert delay, median | 25 days |
| Shortest / longest | 10 days / 132 days |
| Total | 1,474 alert-days |
A note on that 1,474: these are not calendar days but the sum of 37 individual delays, and the intervals overlap each other. So that you can rebuild the table yourself, here are all thirty-seven delays: 10, 18, 19 (ten times), 25 (nine times), 26 (twice), 41, 45, 55 (twice), 60 (twice), 65, 66, 67, 68 (twice), 93, 104, 132 days. Quartiles 19 / 25 / 60, mean 39.8.
The two oldest alerts were both in the ws package — 132 and 104 days. The vulnerable version entered the repository on 15 May along with the short-video pipeline, the first advisory was published on 18 May, and the alert was born on 28 September. 132 days in between.
What's inside those 8.4 hours
Once I looked at that day's commits, the distribution of the measured time didn't flatter me either. The alerts landed at 10:39 Istanbul time. I published an article at 14:35, and another at 17:05. My first touch on dependencies came at 17:52: nodemailer, sharp. Then the critical one — astro together with @astrojs/node, as an atomic pair — at 18:00. The remaining transitive packages at 18:31, the video pipeline's 12 packages at 18:44, the merge at 19:05.
So of those 8 hours 26 minutes, 7 hours 13 minutes is time the alerts sat there while I did something else. The actual work was 1 hour 13 minutes. The dashboard doesn't separate the two, and it doesn't need to: that metric isn't a response time, it's an average age.
One detail from that evening only made sense later: Dependabot had opened five PRs for the video pipeline. I did the upgrades by hand in my own commit, and on seeing that, Dependabot withdrew two of its own PRs the same evening. The remaining three stayed open for nine days until I closed them by hand on 7 October. Not one of the five was merged. Doing it by hand had felt faster that day.
The more telling part is this: 18 of the 37 alerts weren't in the main site at all, but in the separate pipeline I keep for short videos. Less had piled up in the site's own dependencies than in a side tool I run a few times a year. Attention drifts toward the package-lock.json you look at least.
What people who don't pick their own clock do
It helps to look at people who can't set their own stopwatch. The directive CISA issues for federal agencies — no longer BOD 22-01, but BOD 26-04, which replaced it on 10 June 2026 — ties its deadlines to a catalog date. The directive contains this sentence: "Whichever event occurs first starts the remediation timeline." The clock starts either when CISA adds the vulnerability to the KEV catalog or when the agency identifies it on an asset and records it in the central dashboard — whichever happens first. It is easy to skip that second half, but what starts the clock isn't noticing alone; it is logging what you noticed.
That sentence matters because of what it rules out: "I only just noticed" does not reset the clock. Noticing can start the timer, but it cannot stop one that has already started.
I downloaded the catalog and counted it too. The 30 October 2026 release holds 1,766 entries, each with two dates: dateAdded and dueDate. By my own arithmetic the gap between them is most often 21 days (1,025 entries), then 14 days (284), 181 days (238), and 3 days (150). The deadline is nailed to the catalog's own date, not to when the agency got around to looking. I've written about prioritising with the CISA KEV catalog before, through the lens of scores; the question there was which vulnerability matters, and here it's whose calendar decides that it matters.
OWASP's dependency category gets to the same place by a shorter road. One of the first lines under "likely vulnerable" reads: "If you do not know the versions of all components you use…" Not knowing isn't a gap in your information — it is the category itself.
The honest number is in the open alerts
If we're going to talk about bad metrics, I had to look where my own dashboard isn't pointing today. As of 31 October I have 10 open alerts, and none of them enters that 8.4-hour average — by definition the metric only counts the ones that were closed. What stays open stays unmeasured.
Their ages run 31, 11, 10, 9, 9, 5, 5, 3, 3, 3 days — a median under a week. So this isn't one act of neglect; it's a long tail with fresh arrivals behind it. The oldest is ip-address, development scope, open 31 days. Six are vite: six separate advisories, but all six catch the lockfile's 8.2.1 in the same 8.2.x range. One is only low severity, and I'm counting it anyway, because being selective about what you count is precisely what this piece argues against.
Nine have a published patched version; for braces, GitHub lists no patched version at all, so that one genuinely is waiting.
For the other nine I went looking for a technical excuse and couldn't find one. Take vite: the lockfile has 8.2.1, the patched version is 8.2.4, and astro 7.3.5 already asks for ^8.0.13 — the range permits the upgrade. Nothing is holding me back.
Which means my real response time isn't the 8.4 hours I was pleased with. It's 31 days, and counting.
The fix is already written; I'm the one waiting
While preparing this piece I looked at the open pull requests alongside the open alerts. There are five open PRs; three of them are Dependabot's dependency bumps:
| PR | Opened | Change | Age today |
|---|---|---|---|
| #20 | 30 September |
ip-address 10.5.0 → 10.7.2 |
31 days |
| #27 | 21 October |
vite 8.2.1 → 8.3.5 |
10 days |
| #28 | 26 October |
follow-redirects 1.16.0 → 1.16.2 |
5 days |
Those three PRs would close nine of the 10 open alerts: the six vite alerts collapse into one PR, two follow-redirects into another, ip-address on its own. The tenth is the unpatched braces, and no PR exists for it either.
The other two open PRs aren't the robot's — they're mine: one 18 days old, the other 43. I'd just called 31 days a long time; my own branch beats it by half again. I thought I kept security debt as a separate category. It turns out to be one column of the same list.
The robot did its job. There's no red check on any of them either — but the reason turned out to be worse than I assumed. All three carry a single CodeQL check, and on all three the conclusion is neutral, with this line underneath: "2 configurations not found". The scan isn't passing these PRs, it can't evaluate them. For comparison I looked at a PR I opened myself: four analysis runs, all success. On Dependabot's PRs the analysis never runs at all.
A gate that looks green wasn't a gate I passed; nobody ever built one in that corridor. The age of the ip-address PR and the age of its alert are identical: 31 days. The same number sits in two places, one labelled "open vulnerability" and the other "unmerged branch".
To be fair to myself, I do have one hesitation about the vite PR, and I know what it is: the PR doesn't move to the patched 8.2.4 but to 8.3.5. My caution about build-chain upgrades in this repository has a date attached. On 6 July I moved astro to 6.4.8; with @astrojs/node 10.0.5 it threw app.getAdapterLogger is not a function at runtime and the deployed bundle crashed. I reverted the upgrade that day, and the commit message is still there. The build and the type check had both passed; what died was the server itself. The deploy script bringing a new bundle up on a temporary port first, and my marking the critical 28 September upgrade as an "ATOMIC pair", both date from that day.
So a minor jump in the build tool is not zero-risk in this setup. But I can't defend the distance between "not zero-risk" and "let it sit for ten days", because I didn't spend those ten days measuring the risk.
The open PR list is the most honest form security debt takes. The alerts page shows you a number; the PR list shows you a decision. I've measured whether the gates in this repo actually work before, and that piece landed somewhere similar, so I won't rebuild the same finding here.
Who sets the zero point
I found four separate clocks in this setup, and the interesting part isn't what each one measures but who starts it:
| Clock | Where it starts | Started by |
|---|---|---|
| Dependabot MTTR | the moment the alert is created | me (by enabling scanning) |
| Advisory age | the advisory's publication date | upstream |
| KEV due date |
dateAdded, or your own detection — whichever is first |
the catalog |
| Open PR age | the moment the PR is opened | the robot |
Of those four, I start exactly one — and it is precisely the number I was pleased with. Make that your first check too: if the zero point of the metric you're proud of is an action of yours (enabling a scanner, installing a tool, opening a ticket), it is measuring your field of view, not your performance. The second place to look is what never closes: open alerts don't enter the Dependabot average at all, which makes that average an average of good news.
The recipe is short, if you want to repeat it. Pull the alerts with gh api repos/:owner/:repo/dependabot/alerts --paginate (it needs the security_events scope); every record carries both created_at and security_advisory.published_at, and the gap between them is the delay you're after. One step matters more than the rest: published_at does not tell you whether that package was in your tree on that date. So for each advisory date, find the relevant lockfile as it stood that day with git rev-list -1 --before=<date> and compare the version against security_vulnerability.vulnerable_version_range — otherwise you'll blame yourself for a dependency you adopted later. In my case the check confirmed 37 out of 37; in yours it may well eliminate some. Half an hour of work, and sorting the output by advisory date rather than alert date reorders the list completely: ws, which never appears on the dashboard's first page, goes straight to the top.
One thing I diagnosed and did not fix, which I'll record as the honest remainder: no notification is sent for the alerts that pile up at first enablement. Someone like me, switching the lamp on late, meets the largest heap of all through the quietest mail. Whether there's a fix for that, I haven't built one yet.
About this measurement itself
When I switched CodeQL on in the same repository, the lesson was that the finding came from my own patch — the tool worked correctly, I had written it badly. The lesson here isn't about the tool. What Dependabot measures is exactly what it says it measures; its documentation is honest about it. What was wrong was my turning that number into a verdict about myself.
Most metrics have a zero point, and the party that sets it is usually the party being measured. If a number makes you proud, look first at who started the clock. My dashboard said 8.4 hours; the same files in the same repository also said 132 days, and nobody was showing me that one.
Averages are comforting. Zero points aren't — but they're the honest part.
Top comments (0)