Skip to content

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-fix

Parameters ​

ParameterTypeRequiredDescription
fullstringNoSet to true to include per-CVE snapshot data (can be large)

Response ​

json
{
  "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) ​

FieldTypeDescription
labelstringHuman-readable distribution name
familystringDistribution family (nix, debian, fedora, arch, alpine)
totalTrackednumberCVEs tracked for this distro
fixednumberCVEs already fixed in this distro
fixedCountnumberAlias for fixed
unfixednumberCVEs not yet fixed
unknownnumberCVEs 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
fixRatenumber|nullPercentage of decided CVEs that are fixed (0-100). null where withheld
fixRateWithheldstringPresent instead of a rate where fix detection is unsound for this distribution. See below
distinctPackagesnumberHow many distinct packages the row rests on
largestShareobject|nullThe package dominating the row, as { package, cves, share }. A rate resting mostly on one package says more about that package than the distribution
comparableWithstring[]Distributions measured on at least one package in common. Where empty, the figures sit side by side and mean nothing about each other
medianDaysnumber|nullCurrently always null. See medianWithheld
p90Daysnumber|null90th percentile days to fix. null while the median is withheld
medianWithheldstringWhy 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:

DistributionDescription
nixpkgs-unstableNixpkgs unstable channel
nixos-YY.MMLatest two NixOS stable releases
debian_13Debian Trixie
debian_12Debian Bookworm
fedora_44Fedora 44
ubuntu_26_04Ubuntu 26.04 LTS
archArch Linux
alpine_3_24Alpine 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 ​

bash
# 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 ​

javascript
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}`);
  }
}
python
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.