A few months back I was trying to decide whether to keep renting or lock in a mortgage, and every rent-vs-buy calculator I tried online did the same annoying thing: it spat out "you break even in 11 years" with zero visibility into the actual amortization, no accounting for what my down payment would be doing if I invested it instead of handing it to a bank, and no way to model a pre-sale unit — paid off in installments over years before the building even exists — against a plain resale purchase. So I built my own, mostly so I could see the year-by-year numbers instead of trusting a black box.
The mortgage: a real month-by-month amortization, not an annuity shortcut
The monthly payment itself is the standard fixed-rate PMT formula:
const monthlyRate = params.loanRate / 100 / 12;
const n = params.loanYears * 12;
let monthlyMortgage = 0;
if (monthlyRate > 0 && n > 0) {
monthlyMortgage =
loanPrincipal * (monthlyRate * Math.pow(1 + monthlyRate, n)) /
(Math.pow(1 + monthlyRate, n) - 1);
} else if (n > 0) {
monthlyMortgage = loanPrincipal / n; // 0% interest edge case
}
That else if matters more than it looks. Plug a 0% rate into the standard PMT formula and you get 0/0 — undefined, not zero. A couple of countries in the preset list (Japan's default is 0.5%) get close enough to this edge that I didn't want to risk it silently producing NaN, so a zero-or-near-zero rate falls back to straight-line principal division instead.
The bigger thing I didn't want to fake: the loan balance actually amortizes month by month inside the yearly loop, instead of using a closed-form "remaining balance at year N" shortcut.
for (let m = 0; m < 12; m++) {
if (loanBalance > 0) {
const interest = loanBalance * monthlyRate;
const principal = Math.min(monthlyMortgage - interest, loanBalance);
yearMortgage += (principal + interest);
loanBalance = Math.max(0, loanBalance - principal);
}
}
The Math.min on principal and the Math.max(0, ...) on the balance exist for the same reason: on the last payment or two, monthlyMortgage - interest can slightly overshoot what's actually left owed (floating point, or a loan that pays off mid-year), and without those clamps you'd get a negative loan balance and a mortgage payment that's larger than the debt it's paying off.
The renter's cash doesn't just sit there — it's a second portfolio, compounding
This is the part most rent-vs-buy explainers skip, and it's the actual point of the tool: the down payment isn't "spent," it's a choice about where that money lives. If you rent instead, that same cash — plus whatever the buyer pays out in mortgage/tax/maintenance each year that a renter doesn't — goes into an investment account instead of a house.
let rentPortfolio = initialCapital; // same starting cash as the buyer's down payment + costs
// ...each year:
const savingVsBuy = buyExtraThisYear - yearRent; // buyer's yearly outflow minus rent
rentPortfolio = rentPortfolio * (1 + params.investmentReturn / 100) + savingVsBuy;
if (rentPortfolio < 0) rentPortfolio = 0;
buyExtraThisYear is the buyer's total cash out that year — mortgage payment, property tax, maintenance, any lump-sum down payment or transaction cost falling in that year. If that's more than the rent, the difference gets added to the renter's portfolio on top of normal compounding. If rent happens to be more expensive than owning that year (it happens, especially with a cheap fixed-rate mortgage against rising rent), the difference comes back out of the portfolio instead.
The if (rentPortfolio < 0) rentPortfolio = 0 floor is a real modeling choice, not just defensive code — it assumes a renter would stop reinvesting and start pulling from savings/cutting spend rather than going into debt to keep funding the comparison. It also means in an extreme scenario (very cheap mortgage, fast-rising rent) the rent side's net worth gets floored at zero instead of going negative, which quietly flatters the rent scenario at the far edges of the input ranges.
Reverse-engineering a growth rate from "I think it'll be worth this much in 10 years"
Most people don't have an opinion on "3.2% annual appreciation" — they have an opinion on "I think it'll be worth NT$20M in ten years." The tool supports both, and the second mode is a straight CAGR (compound annual growth rate) solve:
const impliedGrowthRate = computed(() => {
if (growthMode.value !== 'target' || params.targetYear <= 0 || params.housePrice <= 0) return '0.00';
const r = Math.pow(params.targetPrice / params.housePrice, 1 / params.targetYear) - 1;
return (r * 100).toFixed(2);
});
That implied rate then gets fed back into the same exponential growth curve used for the "I know the annual %" mode:
const houseValue = P * Math.pow(1 + growthR, yr - 1);
const yearRent = params.monthlyRent * 12 * Math.pow(1 + params.rentGrowthRate / 100, Math.max(yr - 1, 0));
The yr - 1 exponent (not yr) is deliberate: year 1 holds at the price/rent you actually typed in, and compounding only kicks in from year 2 onward. Small detail, but if you're staring at a table checking whether the numbers make sense, a house that "grows" in the very first row before any time has passed reads as a bug even when it's the more common convention.
Pre-sale housing: paying for a place that doesn't exist yet
This is the feature that made me actually finish the tool instead of using someone else's, because pre-sale (buying a unit before it's built, common in Taiwan and mainland China) has a completely different cash-flow shape: no mortgage yet, but a string of installment payments and rent you're still paying somewhere else in the meantime.
const conPctPerYear = params.constructionPayRate / 100 / Math.max(params.constructionYears, 1);
// ...for each year before delivery:
yearExtraDown = P * conPctPerYear;
downPaidSoFar += yearExtraDown;
if (isDeliveryYear) {
const shortfall = downPayment - downPaidSoFar;
if (shortfall > 0) { yearExtraDown += shortfall; downPaidSoFar = downPayment; }
yearExtraDown += txCost;
}
During construction, net worth isn't "home value minus loan balance" (there's no loan yet) — it's home value minus whatever's still owed to the builder:
const builderLiability = P - downPaidSoFar;
buyNet = houseValue - builderLiability - sellCost;
builderLiability is functionally a loan balance, it's just owed to a developer instead of a bank, and it only converts into an actual mortgage at delivery. And because the buyer is still paying rent on top of construction installments before they can move in, break-even for pre-sale is almost always later than for a comparable resale unit — which matches what the tool's own copy tells users to expect.
Where the model quietly simplifies (or is just wrong)
-
The "buying wins from year one" message can't actually fire.
beYearstarts at-1and the loop only ever assigns ityr(1 throughsimYears) or leaves it at-1— it can never become0. But the UI has a whole branch (breakEvenYear === 0→ a blue "buying is better from day one" card) that only triggers on exactly0. As written, the best case you can actually see is "break-even in year 1" rendered with the neutral orange card, not the emphatic blue one. I only found this by tracing the variable, not by looking at the UI. -
Property tax is flat, maintenance isn't. The annual tax figure you type in stays a fixed nominal number for the entire simulation, while maintenance cost is
maintenanceRate * currentHouseValue, so it rises every year with appreciation. That's an inconsistent treatment of inflation between two costs that, in most tax systems, both drift upward over time. - No income tax, no mortgage-interest deduction, no capital gains tax on sale, no tax on rental income or its reinvested returns. Every number in this model is pre-tax. The tool's own "assumptions" footnote says as much, and it's worth taking seriously — tax treatment can shift a multi-decade comparison by years either way.
- Growth is a smooth curve, not real life. Home prices and rent both compound at a constant rate with zero volatility — no crashes, no rent freezes, no vacancy months on the reinvested rental income. Real markets don't move in a straight exponential line, and a single "annual growth %" input can't capture that.
I cleaned up the version I actually use into a small free tool if you want to run your own numbers instead of trusting mine: Rent vs Buy Break-Even Calculator. No sign-up, and it supports both resale and pre-sale.
Available in other languages
- 租屋 vs 買房損益平衡計算機 — 繁體中文
- 租房 vs 买房盈亏平衡计算器 — 简体中文
- Rent vs Buy Break-Even Calculator — English
- 賃貸 vs 持ち家 損益分岐点計算機 — 日本語
- 월세 vs 내집마련 손익분기점 계산기 — 한국어
- Calculateur Louer vs Acheter — Français
- Калькулятор Аренда vs Покупка — Русский
- Mieten vs Kaufen Rechner — Deutsch
- Kalkulator Sewa vs Beli Rumah — Bahasa Indonesia
- Calculadora Alquilar vs Comprar — Español
- Máy Tính Thuê vs Mua Nhà — Tiếng Việt
- เครื่องคำนวณเช่า vs ซื้อบ้าน — ไทย
- Kalkulator Wynajem vs Zakup — Polski
- Kira vs Satın Alma Hesaplayıcısı — Türkçe
- Calcolatore Affitto vs Acquisto Casa — Italiano
- Calculadora Alugar vs Comprar — Português
- Rekenmachine Huren vs Kopen — Nederlands
- Калькулятор Оренда vs Купівля Житла — Українська
Top comments (0)