The Courage to Delete Code: What Dead Code Costs and How to Remove It Safely
By Sergey Nosov
2 October 2026
Ken Thompson designed the first version of Unix. Eric S. Raymond’s The Art of Unix Programming (2003) quotes him: “One of my most productive days was throwing away 1000 lines of code.” Just before the quotation, Raymond makes a related point in his own words: “The most powerful optimization tool in existence may be the delete key.”
It is easy to agree with both and hard to act on either. Adding code feels like progress; deleting code feels like risk. So dead code stays: the method nothing calls, the block commented out “just in case,” the feature flag that has been off for three years. Yet some of the most valuable commits you will ever make are net negative.
This article covers what dead code costs, a company that carried it for nine years, why engineers keep it anyway, and four steps that make deletion safe. It ends with a worked example in C# and a short checklist.
What Counts as Dead Code
For this article, dead code is anything the running system no longer needs:
- unreachable branches and functions that nothing calls;
- commented-out blocks;
- endpoints and features that nobody uses anymore;
- feature flags whose decision was made long ago;
- dependencies that no live code path imports.
Martin Fowler’s catalog of refactorings names the remedy, Remove Dead Code, and its sketch says most of what needs saying: if(false) { doSomethingThatUsedToMatter(); }.
One distinction matters more than the rest. Code that can never run is dead. Code behind a switch that someone could still flip is not dead; it is dormant, sometimes called zombie code. Dormant code is the more dangerous kind, because it can wake up. The story below is about exactly that.
What Dead Code Costs
The instinct says that keeping dead code is free. It is not. In 1988, Edsger W. Dijkstra argued that if we wish to count lines of code, we should regard them not as “lines produced” but as “lines spent.” Dead code keeps costing, year after year, in four currencies:
- A comprehension tax. Every engineer who reads the file spends attention on code that does nothing, and pays again on every visit. Newcomers pay the most, because they cannot yet tell which parts are alive.
- Maintenance drag. Every refactor, migration, and upgrade has to carry the dead weight along. When a type changes, the dead method that uses it has to change too, or the build breaks.
- False signals. A search for a function returns call sites that never run. A security scan reports findings in a module nothing uses. Each false hit costs attention and teaches people to distrust their tools.
- Attack surface. A path nobody uses is a path nobody watches, and nobody patches. Unused does not mean unreachable: an endpoint that no legitimate caller uses anymore still answers anyone who finds it. Improper inventory management is one of the risks in the 2023 edition of the OWASP API Security Top 10. Its description starts with this case: “Threat agents usually get unauthorized access through old API versions or endpoints left running unpatched and using weaker security requirements.”
Those costs accrue slowly. Dormant code can also fail all at once, as one trading firm learned in 2012.
Knight Capital: Nine Years Dormant, Forty-Five Minutes Awake
In 2011 and 2012, according to the Securities and Exchange Commission (SEC), Knight Capital’s trading “generally represented approximately ten percent of all trading in listed U.S. equity securities.” Its automated router, SMARS, took incoming parent orders and sent child orders to trading venues to fill them. Unless noted, everything below comes from the SEC’s order of 16 October 2013.
Inside SMARS sat an old piece of functionality called Power Peg. Knight stopped using it in 2003, but the order records that Knight “elected to leave the Power Peg code on SMARS’s production servers.” The code stayed “present and callable,” and so did the flag that had once switched it on.
Power Peg relied on a cumulative quantity function, which counted the shares of a parent order already filled and told Power Peg when to stop sending child orders. In 2005, Knight moved that function to an earlier point in the SMARS code and did not retest Power Peg. Why would anyone? Nothing used it. Without anyone noticing, the change had “inadvertently disabled the cumulative quantity functionality in the Power Peg code.” From then on, the dormant code had no brake.
In 2012, Knight prepared for the New York Stock Exchange’s Retail Liquidity Program, which was to start on 1 August. The new code was meant to replace Power Peg, and it reused Power Peg’s old flag to switch on the new behavior. “Knight intended to delete the Power Peg code,” the SEC writes. The deletion had finally come, nine years late.
Starting on 27 July 2012, Knight deployed the new code a few servers at a time. A technician did not copy it to one of the eight SMARS servers, and no second technician reviewed the deployment. On the eighth server, Power Peg was still in place, and the old flag still switched it on.
The system tried to say so. Starting at about 8:01 a.m. on 1 August, an internal system sent automated emails that referenced SMARS and an error described as “Power Peg disabled.” Ninety-seven of them reached a group of Knight personnel before the market opened at 9:30 a.m. They were not designed as alerts, and Knight personnel “generally did not review them when they were received.”
At the open, orders carrying the repurposed flag reached the eighth server, and Power Peg woke up. With its brake gone, it “continuously sent child orders, in rapid sequence, for each incoming parent order without regard to the number of share executions Knight had already received.” In about forty-five minutes, 212 parent orders produced millions of child orders: more than four million executions in 154 stocks, for more than 397 million shares. Knight was left holding a net long position of about $3.5 billion and a net short position of about $3.15 billion.
One of Knight’s attempts to fix the problem was a rollback. Knight uninstalled the new code from the seven servers where it had been deployed correctly, and that made things worse. The previous version still contained Power Peg. According to the SEC, the rollback caused “additional incoming parent orders to activate the Power Peg code that was present on those servers.”
On 2 August, Knight put its realized pre-tax loss at approximately $440 million. The SEC’s order later found that Knight “realized a $460 million loss on these positions.” On 6 August, Knight announced $400 million in equity financing from a group of Wall Street firms, money it said would let it “resume normal operations immediately.” On 1 July 2013, Knight Capital Group combined with GETCO Holding Company to form KCG Holdings.
What the Knight Story Teaches
Several failures had to line up:
- code left in place for nine years after its last use;
- a change in 2005 that nobody retested;
- a retired flag given a new meaning;
- a manual deployment that missed one server;
- warnings that went largely unread;
- a rollback that restored the danger.
The SEC’s order names controls that could have stopped the chain, including a written procedure requiring “a simple double-check of the deployment” and monitoring that read the warning emails. Those controls catch slips. The dead code was not a slip; it was a decision, and it stood for nine years. As I read the order, every later failure needed that decision to do harm. Had Power Peg been deleted when it was retired, the missed server and the reused flag would have found nothing to wake. Dead code is not inert; it is loaded.
Two more lessons are easy to miss. First, Knight did try to delete Power Peg in the end. It bundled the deletion with a new feature and a reused flag in one deployment, so the deletion could fail halfway, and it did. A deletion is safest as its own change, verified before anything reuses what it frees. Second, the version you would roll back to is part of what you run. If it still carries dormant code, a rollback can bring that code back to life. And never give a retired flag a new meaning; a new flag costs one line.
Why Engineers Keep Dead Code
If dead code is this expensive, why does it survive? Three fears keep it alive. All three are understandable, and none of them holds up.
“We might need it later.” Extreme Programming named the answer: YAGNI, short for “You Aren’t Gonna Need It.” In Martin Fowler’s summary, it is “a statement that some capability we presume our software needs in the future should not be built now.” The same logic applies to keeping a capability after its need is gone. And if you ever do need the code again, version control has it. As long as you keep the history, deleted code is one git log -S away. Git is the attic; the build is the living room.
“Someone worked hard on that.” That is sunk cost talking. The work is not lost; it is preserved in history, where it belongs. Code does not have to live in the build to be honored.
“What if something depends on it?” Then prove it, one way or the other. That fear points to a gap in tests or telemetry, and the answer to a gap is to close it, not to keep mystery code alive.
Commented-out code deserves a special word, because it is the worst of both worlds. It is invisible to the compiler, so nothing ever compiles or tests it, and it is noise to every reader, so it still costs attention. Version control already keeps the old version, so the comment adds nothing but doubt. Delete it with confidence.
Deleting takes a little courage, and courage is not a fringe idea in software. In the second edition of Extreme Programming Explained, Kent Beck and Cynthia Andres base XP on “the values of communication, feedback, simplicity, courage, and respect.” To my mind, deleting code that no longer earns its place is one of the plainest uses of that value.
How to Delete Safely
Courage without care is just recklessness. Deletion needs a discipline, and mine has four steps.
1. Prove It Is Dead
Start with what tools can prove. Find every usage, including in other repositories. Read the compiler and analyzer warnings instead of suppressing them. The C# compiler reports CS0162, “Unreachable code detected,” for statements it can prove will never run. The .NET analyzer rule IDE0051 flags unused private methods, fields, properties, and events. Like other code-style rules, though, it is off on the command line: Microsoft’s documentation says code-style analysis “is disabled, by default, for all .NET projects on command-line builds.” To run it on build, set the MSBuild property EnforceCodeStyleInBuild to true and raise the rule in .editorconfig with dotnet_diagnostic.IDE0051.severity = warning.
Tools find only what they can prove, and they have blind spots in both directions. A flag stored in a mutable field hides its dead branch from the compiler, and the compiler never sees a comment at all. Meanwhile, code reached through reflection, framework callbacks, or dependency injection can look unused when it is not, and a public method may have callers you cannot see.
For anything with a network edge, measure instead of guessing. Count real traffic on the suspect endpoint or code path, and choose the observation window to fit the code: a year-end job looks dead for eleven months of the year. My Software Development Principles series separates dead code, which is provably unused, from Lava Flow: code of uncertain status that people are afraid to remove. This first step is how Lava Flow becomes dead code you can delete.
2. Delete in Small, Single-Purpose Commits
Make one deletion per commit, with a message your future self can search for, such as Delete BuildV1 (unused since 2024). Small deletions review quickly and revert cleanly; my article on the anatomy of a good code review makes the same case for one intent per pull request. Delete completely: the dead method’s private helpers, configuration, documentation, and tests belong in the same change, or they become the next generation of dead code.
And do not bundle a deletion with new behavior; Knight’s bundled deletion failed on the one server its deployment missed.
3. Let the Nets Catch You
Tests, type checks, code review, and a staged rollout: a deletion earns the same rigor as any other change, no more and no less. A green test suite after a deletion is evidence. If nothing tested the code you deleted, that was part of the problem; Power Peg was not retested after the 2005 change that broke it.
4. Watch Afterward
After the deploy, watch error rates and telemetry. Knight’s systems sent ninety-seven emails naming Power Peg before the market opened, and a warning nobody reads protects nobody.
For public surfaces, such as an HTTP API or a shared library, the polite sequence is deprecate, monitor, and then remove. For HTTP APIs, the Internet Engineering Task Force has published two response headers for the first step. RFC 9745, from March 2025, defines a Deprecation header that carries the date a resource was or will be deprecated. RFC 8594, from May 2019, defines a Sunset header for the date a resource is “likely to become unresponsive.” Deprecation is a signal, not a switch; as RFC 9745 puts it, “the act of deprecation does not change any behavior of the resource.” The headers give callers notice, and your traffic graphs tell you when the last caller has moved on.
One habit prevents a whole class of these problems: give every new feature flag an expiry date on the day it is born. Pete Hodgson’s article on feature toggles, published on Martin Fowler’s site in 2016, makes the economics plain. “Savvy teams view the Feature Toggles in their codebase as inventory which comes with a carrying cost and seek to keep that inventory as low as possible.” Some teams, he writes, go further and create “time bombs” that fail a test, or even refuse to start the application, when a flag outlives its expiration date. The same article cites Knight Capital as “a cautionary tale.” A flag without a removal plan is tomorrow’s Power Peg.
A Worked Example
Here are the members of a small, hypothetical C# class. It has three hazards: a stale flag, a commented-out call kept “just in case,” and an unused twin of the main method.
// TODO: remove; migrated in 2023
static bool useCsv = false;
public Report Build(Order[] items)
{
if (useCsv)
{
// return Csv.Run(items);
return Report.Empty;
}
return Exporter.Run(items);
}
// Unused since 2024; see Build.
Report BuildV1(Order[] items)
{
return Csv.Run(items);
}
Every future reader has to answer three questions here. Can useCsv ever be true? Does anything still need the CSV exporter? Who calls BuildV1? The tools answer only one of them. Built with .NET 10 and IDE0051 enabled as above, this class produces exactly one warning: the private member BuildV1 is unused. The compiler says nothing about the flag: the field is mutable, so the compiler cannot prove the branch dead. And the comment is invisible to the compiler.
Here is the same behavior after deletion:
public Report Build(Order[] items)
{
return Exporter.Run(items);
}
Four lines, and nothing left to explain, patch, or misfire. The deletion cascades, too. With the flag, its branch, and BuildV1 gone, the Csv class has no callers left in this code. IDE0051 looks only at private members, so it will not flag a class, and another project may still call it. That sends you back to step one: search the other repositories, check the telemetry, and delete it in its own commit.
Nothing was lost. If someone needs the old method next year, two commands find it:
git log -S BuildV1 --oneline
git show <commit>~1:src/Reports.cs
The first lists the commits that changed the number of occurrences of the string, including the one that deleted it. The second prints the file as it was just before that commit, ready to copy from. The history keeps the knowledge; the build sheds the risk.
Unused Dependencies Are Supply-Chain Exposure
The same logic applies one level down. A package that no live code path imports still sits in your dependency tree. It is restored on every clean build, it appears in every vulnerability scan, and it needs an upgrade whenever an advisory names it. In ecosystems that run install scripts, it can even execute code on your build machines. For example, npm runs the scripts declared in package.json files unless you turn on its ignore-scripts setting, which is off by default.
Supply-chain attacks make the point concrete. On 23 September 2025, the Cybersecurity and Infrastructure Security Agency (CISA) warned that a self-replicating worm known as Shai-Hulud “has compromised over 500 packages” in the npm registry. The first of its recommendations was to “conduct a dependency review of all software leveraging the npm package ecosystem.” Every package you carry is on that review, whether your code uses it or not.
Tools can find the candidates. Knip, for JavaScript and TypeScript projects, “finds and fixes unused dependencies, exports and files.” In Go, go mod tidy “removes requirements on modules that don’t provide any relevant packages.” Treat each finding like any other suspect: prove it, delete it in its own commit, and watch afterward. Pruning the dependency tree is a security act.
Takeaways
- Deletion is a feature. Less code means less to read, less to test, less to patch, and less to attack. Every line you delete is a line nobody has to secure.
- You are deleting risk, not knowledge. Version control keeps the history. The build should carry only what earns its keep.
- Dormant code is worse than dead code. Code behind a switch that someone could still flip can wake up, and, like Power Peg, it may have broken since anyone last tested it.
- Prove it, then delete it. Prove the code dead with tools and telemetry, delete it in its own small commit, let the usual nets catch you, and watch afterward.
- Never reuse a flag. Give every new flag an expiry date and a removal plan on the day it is born.
- Prune your dependencies. A package nothing imports still carries its risk into every build.
One exercise to finish: pick one dead thing in a codebase you own, such as a stale flag, a commented-out block, or an unused helper. Prove it dead, and delete it in its own reviewed commit. Small, proven, reviewed, and net negative.
Power Peg sat on Knight’s servers for nine years. Removing it would have been an ordinary change on any day in those nine years. Leaving it in place is what gave a missed server and a reused flag something to wake.
Further Reading
- In the Matter of Knight Capital Americas LLC (Securities and Exchange Commission, 16 October 2013)
- Feature Toggles (aka Feature Flags) (Pete Hodgson, 8 February 2016)
- API9:2023 Improper Inventory Management (OWASP, 2023)
- RFC 9745: The Deprecation HTTP Response Header Field (Sanjay Dalal and Erik Wilde, March 2025)
- Basics of the Unix Philosophy, in The Art of Unix Programming (Eric S. Raymond, 2003)