Code churn is the amount of code changed between two versions: statements added, deleted and edited in place. This page defines it, works one example through by hand and by machine, and gives the healthy ranges measured on 136 codebases.
Take two snapshots of a codebase — two releases, two dates, base and head of a pull request. Classify every changed statement into three piles: added (new statements), deleted (removed statements), and changed (statements edited in place). Total churn is the sum. Statements that only move are recorded as moves, not churn; and churn in generated files such as lockfiles is reported separately, so true churn — what developers wrote — can be read on its own. Two ratios matter as much as the volume: REWORK, the share of churn spent editing what already exists, and its complement REP_CHURN, the share that was replacement.
The phrase is used in two ways. Many engineering-metrics tools use it for code that is rewritten soon after it was written. LinearB defines code churn as code “rewritten up until three weeks from when it was first merged”; Hatica as code “rewritten within 21 days of its first merge”. Both also call it rework. That measure follows a team’s recent work over time: how much of what was just merged had to be redone.
This page uses the version-to-version meaning: the statements added, deleted and changed between two versions of the code, whatever their age. It answers a different question — what is different between this release, or this pull request, and the one before it. That is what a reviewer, an auditor or a release manager needs to know, and it is the measure that exposes a hijacked update.
The names overlap. CodeDelta’s REWORK is not the three-week figure: it is the share of version-to-version churn that edited existing statements in place (CHG ÷ CRN).
One small C file, five separate edits. Each edit was measured on its own with the CodeDelta engine (2.5.5) and, for comparison, with git diff --numstat, the line count most churn figures are built on.
#include <stdio.h>
/* Apply the loyalty discount. */
double discount(double total, int years) {
if (years > 5) return total * 0.90;
return total;
}
double shipping(double total) {
if (total > 100.0) return 0.0;
return 4.99;
}
void print_receipt(double total) {
printf("Total: %.2f\n", total);
printf("Thank you\n");
}
double discount(double total,
int years) {
if (years > 5)
return total * 0.90;
The signature and the if are split across lines. Git: 4 lines added, 2 deleted. Statements changed: 0. Behaviour is identical, and the statement count says so.
if (total > 75.0) return 0.0;
The threshold in shipping() drops from 100.0 to 75.0. Git: 1 added, 1 deleted. Statements: 1 changed (CHG). One real decision, counted once.
double tax(double total) {
double rate = 0.20;
return total * rate;
}
Git: 5 lines added, counting the blank line and both braces. Statements: 2 added (ADD) — the declaration and the return.
double shipping(double total) {
if (total > 100.0) return 0.0;
return 4.99;
}
shipping() is cut from the middle of the file and pasted, unaltered, after print_receipt(). Git: 5 added, 5 deleted — ten lines of apparent change. Statements: 0 churn; 2 recorded as moved (MOV), outside the churn total.
printf("Thank you\n");
This line is removed from print_receipt(). Git: 1 deleted. Statements: 1 deleted (DEL).
| Edit | Git lines (+/−) | CHG | DEL | ADD | Churn | Moved |
|---|---|---|---|---|---|---|
| A reformat | 6 | 0 | 0 | 0 | 0 | 0 |
| B edit in place | 2 | 1 | 0 | 0 | 1 | 0 |
| C new function | 5 | 0 | 0 | 2 | 2 | 0 |
| D move | 10 | 0 | 0 | 0 | 0 | 2 |
| E deletion | 1 | 0 | 1 | 0 | 1 | 0 |
| Total | 24 | 1 | 1 | 2 | 4 | 2 |
Twenty-four lines of diff; four statements of real change. Most of the line count came from the reformat and the move, which change nothing the program does. Of the four, one was an edit in place, so REWORK = 1 ÷ 4 = 25%, and REP_CHURN, the share that was addition or removal, = 3 ÷ 4 = 75%.
Most churn figures are line counts taken from a diff. Four things inflate them, and each is shown above or measured in a CodeDelta paper:
Line-based churn lies. Reformat a file — change nothing — and a line counter reports dozens of lines of "churn" where the truth is zero changed statements. Measured at the level of logical statements (LLOC), a pure reformat scores exactly 0, and a one-statement behavioural change scores exactly 1. If a churn number is going to gate a merge or appear in front of an auditor, it has to be built on statements.
| Name | What it counts |
|---|---|
CHG_LLOC | Statements present in both versions but edited |
DEL_LLOC | Statements removed |
ADD_LLOC | Statements added |
CRN_LLOC | Total churn: CHG + DEL + ADD |
MOV_LLOC | Statements moved unaltered; reported beside churn, not inside it |
REWORK | CHG ÷ CRN: the share of change spent editing existing statements |
REP_CHURN | (ADD + DEL) ÷ CRN: the share that was replacement |
TRUE_CHURN | CRN minus churn in generated files such as lockfiles: what developers wrote (true churn) |
The same four counts exist at line level (CHG_SLOC and the rest) for comparison. Statement counting covers 27 languages; the other 35 are measured by line.
Measured across 136 of the largest public codebases, 2010–2026 (every figure reproducible from published commit hashes):
Save the starting file in a folder old, apply one edit to a copy in a folder new, and run:
codedelta old new -o report.html --csv churn.csv
The CSV's TOTAL row holds the counts in the table above. git diff --no-index --numstat old/pricing.c new/pricing.c gives the line figures.
Volume finds the hotspots — where change concentrates, risk concentrates. Shape characterises the process — a codebase whose REWORK falls out of its own historical band has changed how it is developed. And per-file classification turns a release diff into an audit record: what changed, where, by how much, reproducible by anyone from the same two hashes.
A supply-chain attack on a package usually arrives as an ordinary update. The attacker gains the right to publish a trusted library and releases a new version with a small addition: code that steals credentials or cryptocurrency, written to look like the rest of the library. Anyone who installs the update and uses the library runs that code.
Scanning the whole package for dangerous patterns struggles here, because the addition is a few hundred statements inside tens of thousands of legitimate ones. Measuring the change removes the legitimate code from view. Compare the hijacked release with the release before it, and what remains is exactly what the attacker added.
The npm attack of September 2025 shows how plain the signal can be. Five widely used libraries — debug, ansi-styles, color-convert, error-ex and ansi-regex — each published a patch release. In every one, churn put the change in a single file, the library’s main file, and counted the same thing: 230 added statements, with nothing else in the code changed. The five additions were one payload copied into five libraries: the same 76,438-byte line, with an identical SHA-256 fingerprint in all five. The same new code turning up in many places is the mark of a copied payload, and it needs no knowledge of the attacker’s technique to see, only a measurement of what changed.
A small, stable library whose patch release suddenly grows by hundreds of statements in its entry file, or a project in which the same new block appears in file after file, is the pattern to look for. In the same study, the whole-package scan flagged nothing in six of the seven npm hijacks; comparing each hijacked release with its predecessor put the attack in a single file in 7 of the 12 pairs that could be compared.
CodeDelta’s Threat Index turns that observation into a check that runs on every compare. It reports what in a change looks like an attack: the same new code in many files, obfuscated code, a large new file in a quiet release. Tested on 15 real hijacked releases and the latest update of 140 widely used clean packages, it flagged every hijack: 6 at its alarm level, Attention, and 9 as Advisory pointers. Of the 140 clean releases, 2 raised Attention, 21 an Advisory, and 117 nothing.

A Threat Index report generated by CodeDelta 2.5.5 from its Churn + Agent Scan demo, which plants a dependency hijack in the shape of the September 2025 attack. The first finding combines both measurements: churn found the same 5,870 characters of new code added to five library files, and the Agent Scan rated that code HIGH. The demo files are synthetic and inert.
Churn says where the change is, not whether it is malicious; a person still reads the flagged file. It narrows that reading to the files that changed. The full study, with every package, version and fingerprint, is in The change is the threat: 14 hijacked packages, and why you scan what changed.
CodeDelta measures churn, including moves, across 62 languages (27 at statement level), from a CLI, CI pipelines or a GitHub Action on every pull request — proven on single scans up to 43.5 million lines. Run it on your own repository, or start with the papers — the method and every benchmark above are published to be re-run, not believed.