Time-to-Fix Benchmarks
Get cross-distribution time-to-fix benchmarks showing how quickly different Linux distributions patch CVEs.
Endpoint
GET /api/v1/analytics/time-to-fixParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
full | string | No | Set to true to include per-CVE snapshot data (can be large) |
Response
{
"success": true,
"data": {
"aggregates": {
"nixpkgs-unstable": {
"label": "nixpkgs unstable",
"family": "nix",
"totalTracked": 307,
"fixed": 262,
"fixedCount": 262,
"unfixed": 45,
"unknown": 0,
"fixRate": 85,
"distinctPackages": 69,
"largestShare": { "package": "siyuan", "cves": 86, "share": 0.28 },
"comparableWith": ["nixos-26.05", "nixos-25.11", "debian_13"],
"medianDays": null,
"p90Days": null,
"medianWithheld": "fix_date_unknown_share_too_high",
"medianWithheldDetail": "27% of the fixed rows here have no recoverable fix date, above the 25% at which a survival curve stops being an estimate and becomes a guess about the rows it cannot see.",
"population": { "atRisk": 307, "events": 180, "censored": 127, "excluded": 0, "fixDateUnknownShare": 0.274 },
"channelOpenedAt": "2026-04-01T00:00:00.000Z",
"maxFollowUpDays": 173,
"fixedWithinDays": { "30": 0.41, "90": 0.68, "180": null },
"survivalAtEnd": 0.22
},
"debian_13": {
"label": "Debian 13 (trixie)",
"family": "debian",
"totalTracked": 97,
"fixed": 9,
"fixedCount": 9,
"unfixed": 0,
"unknown": 88,
"fixRate": null,
"fixRateWithheld": "Fix status here is derived by comparing the packaged version against the upstream fixed version. This distribution backports security fixes into frozen versions...",
"medianDays": null,
"medianWithheld": "..."
}
},
"totalTracked": 303,
"lastUpdated": "2026-09-21T09:14:22.101Z",
"lastAttempted": "2026-09-21T09:14:22.101Z",
"releaseDrift": [
{ "family": "debian", "tracked": "debian_13", "availableUpTo": "debian_14", "behind": 1 }
],
"releaseDriftCheckedAt": "2026-09-21T09:14:22.101Z"
},
"timestamp": "2026-02-10T12:00:00.000Z"
}Response Fields
Aggregates (per distro)
| Field | Type | Description |
|---|---|---|
label | string | Human-readable distribution name |
family | string | Distribution family (nix, debian, fedora, arch, alpine) |
totalTracked | number | CVEs tracked for this distro |
fixed | number | CVEs already fixed in this distro |
fixedCount | number | Alias for fixed |
unfixed | number | CVEs not yet fixed |
unknown | number | CVEs whose fix status could not be determined, because the packaged and fixed versions cannot be ordered. Kept apart from unfixed: an unanswerable question is not a negative answer |
fixRate | number|null | Percentage of decided CVEs that are fixed (0-100). null where withheld |
fixRateWithheld | string | Present instead of a rate where fix detection is unsound for this distribution. See below |
distinctPackages | number | How many distinct packages the row rests on |
largestShare | object|null | The package dominating the row, as { package, cves, share }. A rate resting mostly on one package says more about that package than the distribution |
comparableWith | string[] | Distributions measured on at least one package in common. Where empty, the figures sit side by side and mean nothing about each other |
medianDays | number|null | null unless a nix channel's survival estimate clears every gate below. See medianWithheld |
p90Days | number|null | 90th percentile days to fix. null while the median is withheld |
medianWithheld | string | For a non-nix distribution, the full sentence explaining why no median is published. For a nix channel, a short machine-readable gate code (no_survival_data, fix_date_unknown_share_too_high, follow_up_shorter_than_horizon, horizon_not_reached) paired with medianWithheldDetail's full sentence |
medianWithheldDetail | string | Nix channels only: the human-readable explanation for medianWithheld's gate code |
population | object | Nix channels only: {atRisk, events, censored, excluded, fixDateUnknownShare} — the exact sample the survival estimate (or its withholding) was computed over |
channelOpenedAt | string|null | Nix channels only: when this channel's first tracked release began |
maxFollowUpDays | number | Nix channels only: how many days of observation the estimate actually has |
fixedWithinDays | object | Nix channels only: {30, 90, 180}, each the share fixed within that many days, or null if maxFollowUpDays hasn't reached that horizon yet (never extrapolated past what was actually observed) |
survivalAtEnd | number|null | Nix channels only: the Kaplan-Meier survival estimate at the end of the observed window |
Why the median is absent
A median computed only over CVEs that were fixed discards the unfixed, which are disproportionately the slow ones, and a clock that runs to the moment this service first observed a fix rather than to the moment anyone shipped one makes every distribution look faster than it is. Neither error is acceptable to publish, so medianDays is null and medianWithheld says why, unless a nix channel's Kaplan-Meier survival estimate (population, channelOpenedAt, maxFollowUpDays, fixedWithinDays, survivalAtEnd) clears every gate in medianWithheld's possible codes, in which case medianDays carries the corrected figure instead.
Non-nix distributions have no survival estimate behind them at all, so medianDays stays null with medianWithheld holding the full explanation directly, rather than a gate code. Withheld rather than removed, so a caller is told the field exists and why it is empty: silently dropping it would leave a reader to conclude we hold no timing data.
Why some fix rates are absent
fixRate is null with a fixRateWithheld note for Debian, Ubuntu, Fedora and the other distributions that backport security fixes into frozen versions. Our detection compares the packaged version against the upstream fixed version, and a fix shipped as 1.19.2-2+deb13u1 for something fixed upstream in 1.21 is invisible to that comparison. A low rate computed this way measures packaging policy rather than responsiveness, so publishing it would invite exactly the wrong conclusion about somebody else's security team.
Tracked Distributions
NixOS channels are computed dynamically based on the current release schedule:
| Distribution | Description |
|---|---|
nixpkgs-unstable | Nixpkgs unstable channel |
nixos-YY | Latest two NixOS stable releases |
debian_13 | Debian Trixie |
fedora_44 | Fedora 44 |
ubuntu_26_04 | Ubuntu 26.04 LTS |
arch | Arch Linux |
alpine_3_24 | Alpine 3.24 |
The tracked release for each family is checked against what Repology actually carries, and releaseDrift in the response names any family that has moved past the release we track, with how far behind it is. A tracked release going stale is how a row quietly starts measuring something three versions old.
Example
# Get aggregate stats only
curl https://api.vulnpatch.dev/api/v1/analytics/time-to-fix
# Include per-CVE snapshots
curl "https://api.vulnpatch.dev/api/v1/analytics/time-to-fix?full=true"Code Examples
async function compareDistros() {
const response = await fetch('https://api.vulnpatch.dev/api/v1/analytics/time-to-fix');
const { data } = await response.json();
for (const [distro, stats] of Object.entries(data.aggregates)) {
// medianDays is null while it is withheld; say so rather than printing
// "null days", and a null fixRate has its own reason attached.
const median = stats.medianDays === null ? `no median (${stats.medianWithheld})` : `median ${stats.medianDays}d`;
const rate = stats.fixRate === null ? `no fix rate (${stats.fixRateWithheld})` : `fix rate ${stats.fixRate}%`;
console.log(`${stats.label}: ${median}, ${rate}`);
}
}import requests
response = requests.get("https://api.vulnpatch.dev/api/v1/analytics/time-to-fix")
data = response.json()["data"]
for distro, stats in data["aggregates"].items():
median = f"median {stats['medianDays']}d" if stats.get('medianDays') is not None else f"no median ({stats.get('medianWithheld')})"
rate = f"fix rate {stats['fixRate']}%" if stats.get('fixRate') is not None else f"no fix rate ({stats.get('fixRateWithheld')})"
print(f"{stats['label']}: {median}, {rate}")Use Cases
- Distribution comparison: Compare patching speed across NixOS, Debian, Arch, etc.
- SLA tracking: Monitor whether your distro meets vulnerability SLAs
- Reporting: Generate time-to-fix trend reports for stakeholders
Caching
Data is computed every 30 minutes via cron.
Related Endpoints
GET /api/v1/fix-etas- Predicted fix timelines for individual CVEsGET /api/v1/package-health- Package maintenance health scoresGET /api/v1/fix-rate- Overall fix rate statistics