cvelistV5 delta.json and deltaLog.json: Sync CVE Changes

The CVE Program's cvelistV5 repository publishes two small index files, cves/delta.json and cves/deltaLog.json, that tell you which CVE records changed and when. We read 30 days of deltaLog (2,870 entries, 122,761 changes) to show what each field means, what 'new' and 'updated' really cover, how often the feed moves, and a tested Python script that keeps a local mirror current from a checkpoint.

Rows of colourful printed cards sorted into clear acrylic racks on wall-mounted wooden shelves
Photo by Jemima Whyles on Unsplash

If you keep your own copy of the CVE List, you do not need to re-download all 421,074 record files (the count on main on November 8, 2026) to stay current. The CVE Program’s cvelistV5 repository publishes two index files, cves/delta.json and cves/deltaLog.json, that list which records were added or modified and when. Store the last fetchTime you processed, read every batch after it, fetch the newest version of each changed record, and fall back to a full baseline when your checkpoint ages out of the log’s 30-day window.

This article explains the fields, what the two change queues really mean, and how the feed behaves in practice, based on the copy of deltaLog.json we downloaded on November 8, 2026 (2,870 batches, October 9 to November 8). It ends with a sync script we ran against that feed. Where a record’s fields fit into a patch decision, see our vulnerability prioritization workflow from CVE record to ticket.

The two files and how they relate

FileWhat it holdsSize on Nov 8, 2026
cves/delta.jsonThe single most recent change batch684 bytes (2 changes)
cves/deltaLog.jsonAn array of change batches, newest first, for a rolling 30 days36.8 MB (2,870 batches)

Both files sit on the main branch and are readable over raw.githubusercontent.com without authentication. On the morning we checked, delta.json was byte-for-byte the same object as the first element of deltaLog.json.

The 30-day window comes from two places. The repository README says the log “normally retains a rolling 30 days worth of CVE record modification history”, and notes that it was temporarily cut to 15 days during a date-normalization exercise in February 2026. The repository’s update.yml workflow sets CVES_DEFAULT_DELTA_LOG_HISTORY_IN_DAYS: 30. The window is a setting, so code that assumes exactly 30 days will eventually be wrong. Check the timestamp of the oldest entry instead.

Fields in a delta batch

Each batch (the whole of delta.json, or one element of deltaLog.json) looks like this. The example is the real delta.json from 03:15 UTC on November 8, 2026, trimmed to one item:

{
  "fetchTime": "2026-11-08T03:15:41.690Z",
  "numberOfChanges": 2,
  "new": [],
  "updated": [
    {
      "cveId": "CVE-2024-28834",
      "cveOrgLink": "https://www.cve.org/CVERecord?id=CVE-2024-28834",
      "githubLink": "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/2024/28xxx/CVE-2024-28834.json",
      "dateUpdated": "2026-11-08T03:06:52.175Z"
    }
  ],
  "error": []
}
FieldMeaningWhat to do with it
fetchTimeWhen the update job pulled changes from CVE Services (UTC)Your checkpoint. Store the newest one you processed.
numberOfChangesCount of items in new + updated + errorA sanity check; it matched the list lengths in all 2,870 batches we read.
newRecords whose JSON file was added to the repository in this batchFetch and insert. Do not assume they are newly disclosed vulnerabilities (see below).
updatedRecords whose existing file was modifiedFetch and replace your copy.
errorItems the tooling could not classifyLog and alert. Empty in every batch we read.
cveIdThe CVE IDYour deduplication key.
githubLinkRaw URL of the record JSON on mainFetch this to get the current record.
cveOrgLinkThe record’s page on cve.orgFor humans and tickets.
dateUpdatedThe record’s cveMetadata.dateUpdated when the batch was builtUse it to keep only the newest change per CVE.

Every one of the 122,761 change items we read had exactly those four keys. None of them tells you what changed in the record. If you need a diff, keep the previous version and compare.

What “new” and “updated” actually mean

The labels come from git, not from the CVE lifecycle. The repository’s bundled tooling (.github/workflows/dist/index.js) builds each delta from git status: files that were not in the repository go to the new queue, modified files go to updated. Two consequences matter for triage.

A “new” record can be REJECTED. We fetched 300 randomly sampled IDs from the new queue in the most recent 600 batches. 272 are PUBLISHED records today and 28 are REJECTED. A reserved ID that is rejected before publication still lands as a new file. If you feed “new” straight into an alerting channel, filter on cveMetadata.state first. Our guide to CVE record states covers what each state means for a ticket.

Most updates are enrichment. We fetched 300 sampled IDs from the updated queue (299 parsed) and compared each record’s cveMetadata.dateUpdated with the timestamps on its containers. In 244 records the closest match (within two minutes) was the CISA-ADP container, in 51 it was the CNA container and in 4 it was the CVE Program container. In other words, the typical update is CISA adding or revising SSVC, KEV or CVSS data rather than the vendor changing the description or affected versions. That is useful: an update can be the moment a CVE gains an SSVC Exploitation: active value or a KEV entry. See how ADP containers work and how to read the SSVC values CISA adds.

Both samples reflect the records as they stood when we fetched them on November 8, 2026, which can be later than the batch that listed them. Treat the proportions as indicative of one month, not a constant.

How the feed behaves

We counted the whole log with Python (the counts below are recomputed from the file, not estimated).

Measure (Oct 9 to Nov 8, 2026)Value
Batches2,870
Items in new17,699
Items in updated105,062
Items in error0
Distinct CVE IDs touched31,848
IDs that appear in more than one batch21,774
Most appearances for one ID118 (two IDs, each updated roughly every 6 hours)
Median gap between batches10.2 minutes
90th percentile gap28.9 minutes
Longest gap228.8 minutes
Largest single batch1,417 changes (Oct 13, 21:42 UTC)

Three points follow for anyone building on the feed:

  1. The same CVE appears many times, and each appearance is a real change. No (cveId, dateUpdated) pair repeated across the 30 days. If you process batches one by one you will fetch the same record repeatedly; collapse to the newest dateUpdated per ID first.
  2. Schedule is not cadence. update.yml is set to run every five minutes, but the gaps above show batches arriving anywhere from 2 minutes to nearly 4 hours apart. Alert on “no new batch for several hours”, not “no batch in 5 minutes”.
  3. dateUpdated trails fetchTime slightly. Across all items the median lag from a record’s dateUpdated to the batch’s fetchTime was 4.8 minutes and the maximum was about 102 minutes. That is fine for vulnerability management, but it is not a real-time feed.

The other ways to sync

The README offers two download routes, and the delta files sit alongside them.

  • git. Clone once, then git pull. Git works out what changed for you. A shallow, blob-less clone keeps the first download small, but a full working tree still means checking out more than 400,000 JSON files.
  • Release zips. The README says a baseline named YYYY-MM-DD_all_CVEs_at_midnight.zip is “issued at the end of each day at midnight” and that hourly files named like 2024-04-04_delta_CVEs_at_0100Z.zip “only [contain] the deltas since the baseline zip file”. We downloaded 2026-11-08_delta_CVEs_at_0300Z.zip from the release tagged cve_2026-11-08_0300Z: it held 194 records under deltaCves/, and all 194 IDs appear in deltaLog.json batches between midnight and 03:30 UTC. Note the hourly zip is cumulative since midnight, not just the last hour. In the release assets we fetched, the baseline file actually carried a doubled extension (2026-11-08_all_CVEs_at_midnight.zip.zip).
  • delta files over HTTPS. Best when you only want the records that changed and do not want a git clone at all, for example in a serverless function or a scanner plugin.

A tested sync script

The script below keeps a local folder of records current from a checkpoint. It uses only the Python standard library.

#!/usr/bin/env python3
"""Incremental CVE record sync from cvelistV5's cves/deltaLog.json (Python 3.9+, stdlib only)."""
import json, pathlib, sys, urllib.request
from datetime import datetime

RAW = "https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/deltaLog.json"
STATE = pathlib.Path("state.json")      # remembers the last fetchTime we processed
MIRROR = pathlib.Path("records")        # one JSON file per CVE ID

def ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))

def get_json(url):
    with urllib.request.urlopen(url, timeout=60) as r:
        return json.load(r)

def main():
    state = json.loads(STATE.read_text()) if STATE.exists() else {}
    log = get_json(RAW)                              # newest entry first
    oldest = ts(log[-1]["fetchTime"])
    since = ts(state["lastFetchTime"]) if "lastFetchTime" in state else None
    if since is None or since < oldest:
        sys.exit("No checkpoint, or checkpoint older than the log window: "
                 "load a midnight baseline zip first, then set lastFetchTime.")

    # Collect every change after the checkpoint; keep the newest dateUpdated per CVE.
    todo = {}
    for entry in log:
        if ts(entry["fetchTime"]) <= since:
            break
        for queue in ("new", "updated"):
            for item in entry[queue]:
                cur = todo.get(item["cveId"])
                if cur is None or ts(item["dateUpdated"]) > ts(cur["dateUpdated"]):
                    todo[item["cveId"]] = item
        if entry["error"]:
            print("delta reported errors:", entry["error"], file=sys.stderr)

    MIRROR.mkdir(exist_ok=True)
    counts = {}
    for cve_id, item in todo.items():
        record = get_json(item["githubLink"])
        state_name = record["cveMetadata"]["state"]          # PUBLISHED or REJECTED
        counts[state_name] = counts.get(state_name, 0) + 1
        (MIRROR / f"{cve_id}.json").write_text(json.dumps(record))

    STATE.write_text(json.dumps({"lastFetchTime": log[0]["fetchTime"]}))
    print(f"{len(todo)} records refreshed since {since.isoformat()}: {counts}")

if __name__ == "__main__":
    main()

How we tested it. On November 8, 2026 we ran it with Python 3 on Linux, with a checkpoint set to the sixth-newest batch (2026-11-08T01:47:15.999Z). It downloaded the log, refreshed 29 records (all PUBLISHED) in about five seconds, and moved the checkpoint to 2026-11-08T03:15:41.690Z. With the state file deleted it exited with the re-baseline message, as intended.

Limits of the script. It is a starting point, not a production sync:

  • It downloads the whole 36.8 MB log on every run. For runs every few minutes, poll delta.json and only pull the log when you have missed batches.
  • It fetches records one at a time and does not retry. Add parallelism, retries and backoff before you point it at a large backlog.
  • githubLink points at main, so you get the record as it is when you fetch it, which may be newer than the batch. That is usually what you want, but it means dateUpdated in your copy can be later than the one in the delta.
  • It does not handle the rare case of a record file being removed from the repository; the delta format has no “deleted” queue.

Where this fits in a vulnerability workflow

A local, current mirror is what makes the rest of the prioritization stack cheap: join it to the CISA KEV JSON feed and the EPSS daily file, and re-run the decision only for the CVE IDs that the delta says changed. Because most updates are CISA-ADP enrichment, a changed record is worth re-scoring even when the vendor text did not move.

Sources and verification

Important claimSourceStatus
deltaLog retains a rolling 30 days; baseline and hourly zip naming; hourly zip holds deltas since the baselinecvelistV5 READMEVerified
Update job scheduled every 5 minutes; 30-day history settingcvelistV5 update.ymlVerified
new = added file, updated = modified filecvelistV5 workflow tooling, dist/index.jsVerified (bundled source read on Nov 8, 2026)
Field names, counts, gaps, largest batchcves/deltaLog.json, downloaded Nov 8, 2026Verified (computed)
28 of 300 sampled new IDs REJECTED; 244 of 299 sampled updates match CISA-ADPSampled record files from cvelistV5 main, fetched Nov 8, 2026Qualified (random sample, current record state)
Hourly zip contents (194 records, all in deltaLog)cvelistV5 releases, tag cve_2026-11-08_0300ZVerified

Frequently asked questions

What is the difference between delta.json and deltaLog.json in cvelistV5?

delta.json holds only the latest change batch written by the repository's update job. deltaLog.json is an array of those batches, newest first, retained for a rolling 30 days according to the repository README and the update workflow's CVES_DEFAULT_DELTA_LOG_HISTORY_IN_DAYS setting. On November 8, 2026, delta.json was identical to the first element of deltaLog.json.

Does 'new' in cvelistV5 deltas mean a newly published CVE?

No. The repository tooling puts a CVE in the new queue when its JSON file is added to the repository, and in updated when an existing file is modified. A reserved ID that is rejected without ever being published still arrives as a new file. Check cveMetadata.state in the record itself.

How often does cvelistV5 update?

The update workflow is scheduled every five minutes, but scheduled runs do not land that regularly. Across the 30 days of deltaLog we read, the median gap between batches was about 10 minutes, the 90th percentile about 29 minutes and the longest gap about 3 hours 49 minutes.

What if my sync has been down for more than 30 days?

The log can no longer tell you everything that changed. Download the most recent midnight baseline zip from the repository's releases (or git pull a clone), replace your mirror, and restart the checkpoint from the newest fetchTime in deltaLog.json.

CveCvelistv5Cve Json 5Vulnerability ManagementData FeedsAutomation