A while back I needed a DSCR calculator that wasn't buried under popups and didn't ask me to create an account before showing a single number. So I built one: a free, no-signup debt service coverage ratio calculator for rental property investors. It runs entirely in the browser.
It sounded like a weekend project. The math had opinions.
Here's what I learned, with the actual logic.
What DSCR actually is
DSCR (debt service coverage ratio) answers one question: for every dollar of loan payment a rental property owes, how many dollars of operating income does it produce?
DSCR = Net Operating Income / Annual Debt Service
- Net operating income (NOI) = annual rent minus operating expenses (taxes, insurance, maintenance, management, vacancy). Not minus the mortgage — the mortgage is what we're measuring against.
- Annual debt service = the yearly loan payment, principal + interest.
A DSCR of 1.25 means the property produces $1.25 for every $1.00 of debt payment. Most investment-property lenders want to see 1.25x. Below 1.00, the property can't cover its own loan.
The amortization formula (the part everyone gets wrong)
The monthly payment on a fixed-rate loan comes from the standard amortization formula:
M = P * r(1+r)^n / ((1+r)^n - 1)
Where P is principal, r is the monthly rate, n is the number of payments. In code:
function monthlyPayment(principal, annualRatePct, years) {
const r = annualRatePct / 100 / 12;
const n = years * 12;
if (r === 0) return principal / n; // edge case: 0% loans
const factor = Math.pow(1 + r, n);
return (principal * r * factor) / (factor - 1);
}
That r === 0 guard matters. Without it, a 0% rate gives you 0/0 = NaN, and your calculator silently shows nothing. Real users do type 0 — usually to model an all-cash scenario or a family loan.
Floating point money is a lie (so round deliberately)
JavaScript's floating point will happily tell you a monthly payment is 4055.459999999999. For money, I round only at display time and keep full precision internally:
const annualDebtService = monthlyPayment(580000, 7.5, 30) * 12;
// => 48665.51...
const dscr = 62400 / annualDebtService;
// => 1.2824...
Display 1.28, compute with 1.2824. Rounding intermediate steps is how you get calculators that disagree with each other by a cent and erode trust.
The full calculation
function calculateDSCR({ annualRent, operatingExpenses, loanAmount, annualRatePct, years }) {
const noi = annualRent - operatingExpenses;
const annualDebtService = monthlyPayment(loanAmount, annualRatePct, years) * 12;
return {
noi,
annualDebtService,
dscr: noi / annualDebtService,
};
}
// $96k rent, $33.6k expenses, $580k at 7.5% for 30 years
calculateDSCR({ annualRent: 96000, operatingExpenses: 33600, loanAmount: 580000, annualRatePct: 7.5, years: 30 });
// => { noi: 62400, annualDebtService: 48665.51, dscr: 1.2824 }
The domain trap: two methods, one name
The trickiest bug wasn't in the code — it was in the domain. There are two different DSCR methods, and they give different answers on the same property:
- NOI method: subtract operating expenses (including taxes and insurance) from income first, divide by principal + interest.
- Rent-over-PITIA method: divide gross rent by PITIA (principal, interest, taxes, insurance, association dues). Nothing subtracted from income.
Same property, same loan, different ratios. I made the calculator use the NOI method and explain both, because investors who don't know the difference discover it mid-underwriting — when the deal is already under contract.
Working backwards: the feature users actually want
The most useful thing the calculator does isn't computing DSCR from inputs — it's inverting the formula. Given a target DSCR (say 1.25), what's the maximum loan the property supports?
function maxLoan(noi, targetDSCR, annualRatePct, years) {
const maxAnnualPayment = noi / targetDSCR;
const r = annualRatePct / 100 / 12;
const n = years * 12;
const factor = Math.pow(1 + r, n);
// invert the amortization formula to solve for P
return (maxAnnualPayment / 12) * (factor - 1) / (r * factor);
}
maxLoan(62400, 1.25, 7.5, 30); // => ~$594,953
That number is the investor's negotiation ceiling. If the seller won't meet it, the math already said no.
Lessons
- Guard your edge cases — 0% rates, zero expenses, and empty inputs all produce NaN if you let them.
- Round at display, never in the pipeline — intermediate rounding is how calculators disagree.
- The domain is harder than the code — the two-method trap took more research than the entire amortization implementation.
- Show your math — every existing calculator I found hid its formula. Showing a worked example is what makes users trust the output.
The whole thing is a single HTML file with vanilla JavaScript — no framework, no build step, no backend. Financial data never leaves the browser, which is the entire point.
Try it live: DSCR Calculator — free, no signup. And if you spot a math edge case I missed, I'd genuinely like to hear about it in the comments.
Top comments (0)