Back to Blog
Technical DebtEngineering LeadershipBusinessStrategy

The Real Cost of Technical Debt: A CFO-Friendly Guide

Put a price on technical debt with data you already have: git hotspots, CI times, incidents and late features, in a one-page case your CFO will accept.

P
Davi Nunes
March 16, 2026Updated October 30, 202611 min read

Your CFO does not need to understand coupling or test coverage to make a good decision about technical debt. They need three numbers: what the debt costs per quarter, what it costs to fix, and how you will know the fix worked. Most engineering teams never produce those numbers, so the request arrives as "we need a refactoring sprint", and it loses to the next feature on the roadmap.

You already have the data: git history, CI pipeline records and incident reviews. This guide turns them into a cost model finance can check, a register that keeps it current, and a one-page case for paying it down.

What Counts as Debt, and What Does Not

Technical debt is a shortcut in the code that makes future work slower or riskier. Like a loan, it has two parts:

  • Principal is what it would cost to fix the shortcut now: the engineer-weeks to replace the hard-coded rules, add the missing tests, or split the module.
  • Interest is what you pay every month you do not: the extra hours on every change, the incidents, the features that ship late.

It arrives in two ways. Deliberate debt is a trade-off made on purpose and written down: "we hard-code VAT for Portugal only to launch in March; revisit before we sell in Spain." Accidental decay is nobody's decision: dependencies left to age, a module that grew without an owner, a design sized for a tenth of today's traffic. Both cost money, but decay is the one that surprises budgets, because nobody recorded it.

Many things engineers complain about are not debt in this sense:

  • Ugly code nobody touches. If a file has not changed in two years, it charges no interest. Leave it alone.
  • Missing features. Something the product does not do yet is roadmap, not debt.
  • Bugs. A defect becomes a debt signal only when the same area keeps producing them.
  • Old technology that is still supported and still fits. Age alone costs nothing.

The rule the rest of this guide depends on: interest is only paid where code keeps changing. Your most expensive debt is not your worst code. It sits in the files your team edits every week.

A Cost Model a CFO Will Accept

Finance accepts a model whose inputs trace to a source. Use three lines: friction, incidents and delay. Every number below is an example: replace each with your own.

Start with the loaded hourly cost of an engineer: salary plus employer contributions, equipment, licenses and office, divided by the hours actually available for work after holidays and meetings. Finance can give you this figure. Say it is €75.

Friction. A team of 8 engineers loses 4 hours each per week to one debt item: waiting on slow builds, manual regression passes, working around a module nobody dares to change. Over a quarter of 12 working weeks: 8 × 4 × 12 × €75 = €28,800.

Incidents. Count only incidents whose review names the debt item as a cause. Say there were 3 this quarter. Each took 12 engineer-hours (€900) and had €1,600 of direct business impact from your own records: refunds, service credits, orders lost during the outage. That is 3 × €2,500 = €7,500. Leave incident hours out of the friction line so no hour is counted twice.

Delay. A feature whose business case promised €6,000 a month of gross margin shipped one month late, and the team can show the delay came from the debt item. That is €6,000. Use the original business case figure, not a new estimate made for this argument.

Interest for the quarter: €28,800 + €7,500 + €6,000 = €42,300, about €14,100 a month.

The principal: fixing the item takes 2 engineers for 6 weeks, so 12 engineer-weeks × 35 hours × €75 = €31,500.

Claim only the reduction you will measure. Say the fix halves the friction and ends the incidents: €14,400 + €7,500 = €21,900 a quarter, or €7,300 a month. The payback period is €31,500 ÷ €7,300, a little over four months. Leave delay out of the payback: past delays do not come back.

Fill this in with your own figures:

text
Friction   = engineers x hours lost per week x working weeks x loaded hourly cost
Incidents  = incidents linked to the item x (engineer hours x hourly cost + business impact)
Delay      = months of delay x monthly value from the feature's own business case
Interest per quarter = Friction + Incidents + Delay
Principal  = engineer-weeks to fix x available hours per week x loaded hourly cost
Payback in months = Principal / (interest per month the fix will remove)

Where the Numbers Come From

Every input comes from systems you already run.

Change hotspots from git

This counts how many commits touched each file over the last year:

bash
git log --since="12 months ago" --format= --name-only | grep -v '^$' | sort | uniq -c | sort -rn | head -20

Ignore lockfiles and generated code; the rest shows where your team spends its time. Frequent change alone is not debt, so add two signals: how many of those commits were bug fixes, and how large the file is.

bash
cd "$(git rev-parse --show-toplevel)" || exit 1
since="12 months ago"
git log --since="$since" --format= --name-only | grep -v '^$' | sort | uniq -c | sort -rn | head -20 |
while read -r changes file; do
  [ -f "$file" ] || continue
  fixes=$(git log --since="$since" -i -E --grep='fix|bug' --format=%h -- "$file" | wc -l)
  lines=$(wc -l < "$file")
  printf '%7s %5s %7s  %s\n' "$changes" "$fixes" "$lines" "$file"
done

The columns are changes, fix commits, lines and path. A large file that changes every week and keeps needing fixes is charging interest. A large file that never changes is not. A complexity score beats line count, if you have one.

Know the limits of the fix count. It depends on commit discipline. The pattern also matches "prefix", "debug" and "fixture". Squash merges collapse many commits into one, and -- "$file" does not follow renames. If your team uses conventional commits, --grep='^fix' is more precise. Treat the number as a ranking, not a measurement.

To turn hotspots into hours, compare ticket cycle times: if pricing-module changes take a median of six days and comparable changes elsewhere take two, the difference is friction you can count.

CI duration

Export main-branch pipeline durations for the past year and plot the weekly median. Price the wait: if the median went from 8 to 20 minutes, your team runs 30 pipelines a day that someone waits for, and you assume half of each wait is lost to context switching, that is 30 × 12 × 0.5 = 180 minutes a day. That is 15 hours a week, nearly 2 of the 4 hours per engineer in the example. CI wait is an input to friction, not a separate cost.

Change failure rate and lead time

Change failure rate is the share of production deployments that needed a rollback, revert or hotfix. Your deployment history gives the denominator. For a lower bound on the numerator, count reverts:

bash
git log main --since="3 months ago" --oneline --grep='^Revert "' | wc -l

The count misses rollbacks done by redeploying an older version and fixes shipped as new commits. Lead time is the time from merge to production, which your CI deploy jobs record. Split both by area: if changes to hotspot files fail or wait more often than the rest, that difference is interest.

Onboarding time

Note each new engineer's start date, then find their first commit:

bash
git log --reverse --author="new.joiner@example.com" --format='%ad' --date=short | head -1

Track it per joiner, with the time until they change a hotspot file without pairing. If it keeps growing, you pay again with every hire.

Get these numbers for your own codebase. Request a free technical debt assessment: we run the hotspot and fix-frequency analysis on your repositories, look at your CI duration trend and change failure rate, and price your top debt items with your own loaded cost.

Keep a Debt Register

The numbers go stale unless they live somewhere. Each entry needs: where it is, a symptom anyone can observe, interest per month with its evidence, an estimated principal, an owner, and a decision.

yaml
- id: TD-021
  where: services/orders/erp-sync/
  type: accidental        # or: deliberate
  recorded: 2026-09-14
  symptom: >
    Every change to the order export needs a manual end-to-end check;
    2 of the last 6 incidents started here.
  interest_per_month:
    hours: 30
    eur: 2250
    evidence: hotspot report, incident reviews, ticket cycle time
  principal_estimate:
    engineer_weeks: 4-6
  owner: orders-lead
  decision: add contract tests now, then replace the sync job in stages
  review: 2026-12-01

To keep it alive:

  • Keep it next to the code, as a file in the repository or as labeled issues, where engineers already work.
  • Make deliberate debt pay its way in. A shortcut taken on purpose needs an entry with an owner and a review date before it merges.
  • Review it monthly for thirty minutes. Rerun the hotspot script, update the interest on the top entries, and give every ownerless entry an owner or close it.
  • Record the result. When an item is paid, write the measured effect next to the estimate. That track record gets the next proposal approved.

Paying It Down

A fixed share of capacity

Reserve a share of every iteration for register items and stop it from being the first thing cut. Size it from your own interest: if friction costs each engineer 4 of 35 hours a week, a share of around 10 to 15 percent means you spend on principal roughly what you already lose to interest.

Pay down in the hotspots you are already changing

When a ticket touches a top-twenty hotspot, its estimate includes cleaning up that area. The context is loaded and the tests are running anyway, so this is the cheapest paydown there is, and it lands exactly where the interest is.

Strangle large rewrites

For a module too large to fix in one go, put a facade in front of it and move one route, capability or customer segment at a time to the new implementation. Each step ships and can be measured, and the old code shrinks until it can be deleted. This is how a staged move from monolith to microservices avoids the big-bang rewrite, the highest-variance project you can put in front of a CFO.

Stop new debt in review

Paying down is wasted if new debt arrives at the same rate. Put the rules in CI so they do not depend on a reviewer's mood: tests required for changed code, dependency updates on a schedule, and no anonymous TODOs. This check fails a merge request adding a TODO or FIXME without a ticket ID (fetch the target branch in the job first):

bash
diff=$(git diff origin/main...HEAD -U0) || exit 1
if printf '%s\n' "$diff" | grep -E '^\+' | grep -v '^+++' | grep -E 'TODO|FIXME' | grep -vE '(TODO|FIXME)\([A-Z]+-[0-9]+\)'; then
  echo "TODO/FIXME without a ticket ID, write it as TODO(PAY-123)"
  exit 1
fi

The review habits that stop debt forming are in our clean code rules for production teams.

Presenting It on One Page

Bring finance one page per proposal, in this order:

  1. 1Cost per quarter. The three lines, each with its source: hotspot report, incident reviews, the feature's business case.
  2. 2Risk. What gets worse if nothing changes: the incident trend in this area, a runtime approaching end of support, knowledge held by one person.
  3. 3The proposal. Principal as a range, people and duration, what is deferred to fund it, and the point where you stop if the numbers do not move.
  4. 4How you will measure the effect. The baseline today and the same measures at 30, 60 and 90 days: hours lost, incidents in the area, lead time, CI duration.
  5. 5The decision you need. One sentence.

What not to say to a finance audience:

  • "The code is a mess" or "we need to refactor." That describes your frustration, not a cost.
  • Story points and velocity. Internal units finance cannot audit.
  • "Savings." Engineers are paid either way. Say "capacity returned to the roadmap" unless the hiring plan actually changes.
  • Industry averages. Someone else's numbers invite questions you cannot answer. Your own data survives them.
  • A single precise figure. Give a range and state the assumptions.

Start With One Item

Do not price the whole codebase. Take the top entry from the hotspot report, run it through the model with your own loaded cost, and bring one page to your next budget conversation. If you are planning new web development on a codebase that already carries this debt, price the debt before the new work lands on top of it.

Book a free technical debt assessment and we will run the analysis on your repositories, build the first register entries with you, and draft the one-page case for your top item, ready for your CFO.