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": "The median was computed only over CVEs that were fixed, and its clock ran to the moment this service first observed a fix rather than to the moment the distribution shipped one..."
},
"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 | Currently always null. See medianWithheld |
p90Days | number|null | 90th percentile days to fix. null while the median is withheld |
medianWithheld | string | Why no median is published |
Why the median is absent
medianDays is null for every distribution, and medianWithheld says why. The figure it replaced was wrong in three ways at once: it was computed only over CVEs that were fixed, discarding the unfixed, which are disproportionately the slow ones; its clock ran to the moment this service first observed a fix rather than to the moment anyone shipped one; and the N of M fixed count beside it described a different sample. Debian rendered as the fastest distribution tracked while having the lowest fix rate of any of them.
It is withheld rather than corrected because correcting it is a decision about what we publish concerning other people's projects. A corrected survival estimate is in progress. 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.MM | Latest two NixOS stable releases |
debian_13 | Debian Trixie |
debian_12 | Debian Bookworm |
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