When Can I Retire?

The age you can retire on your numbers: savings today, what you put away each month, expected return, inflation and what you'll spend. A year-by-year chart of your savings against the nest egg you need, and what $500 more a month would change.

5 steps · shared by Klajdi · October 2, 2026
Make it your own →
No one's real data is in this template. It contains the recipe — steps, column names and SQL logic (plus sample data for demos). When you run it, your data is processed in your own browser; your files are never uploaded.

How it works — every step, readable

  1. 01Assumptions — edit these
    SELECT * FROM (VALUES
      ('Your age today', 40, 'years'),
      ('Savings today', 200000, 'USD'),
      ('You save per month', 2000, 'USD'),
      ('Expected return', 7, '% per year'),
      ('Inflation', 2.5, '% per year'),
      ('Spending in retirement', 70000, 'USD per year, in today''s money'),
      ('Other income in retirement', 25000, 'USD per year, in today''s money (pension, Social Security)'),
      ('Safe withdrawal rate', 4, '% per year')
    ) AS t(assumption, value, unit)
  2. 02Savings vs the nest egg you need, year by year
    WITH p AS (
      SELECT
        MAX(CASE WHEN assumption = 'Your age today' THEN CAST(value AS DOUBLE) END) AS age0,
        MAX(CASE WHEN assumption = 'Savings today' THEN CAST(value AS DOUBLE) END) AS s0,
        MAX(CASE WHEN assumption = 'You save per month' THEN CAST(value AS DOUBLE) END) AS m,
        MAX(CASE WHEN assumption = 'Expected return' THEN CAST(value AS DOUBLE) END) / 100.0 AS r,
        MAX(CASE WHEN assumption = 'Inflation' THEN CAST(value AS DOUBLE) END) / 100.0 AS infl,
        MAX(CASE WHEN assumption = 'Spending in retirement' THEN CAST(value AS DOUBLE) END) AS spend,
        MAX(CASE WHEN assumption = 'Other income in retirement' THEN CAST(value AS DOUBLE) END) AS other,
        MAX(CASE WHEN assumption = 'Safe withdrawal rate' THEN CAST(value AS DOUBLE) END) / 100.0 AS swr,
        CAST(strftime(current_date, '%Y') AS INTEGER) AS y0
      FROM input_1
    ),
    sc AS (SELECT * FROM (VALUES ('plan', 0, 0.0), ('save_500_more', 500, 0.0), ('earn_1pct_more', 0, 0.01)) AS t(scenario, extra_m, extra_r)),
    g AS (
      SELECT sc.scenario, n, p.age0 + n AS age, p.y0 + n AS year,
        p.m + sc.extra_m AS m2, p.r + sc.extra_r AS r2, p.*
      FROM p CROSS JOIN sc CROSS JOIN generate_series(0, 60) AS s(n)
      WHERE p.age0 + n <= 95
    ),
    proj AS (
      SELECT scenario, n, age, year,
        -- savings if you keep saving until this age: today's pot grown, plus the monthly contributions grown
        CASE WHEN r2 = 0 THEN s0 + 12 * m2 * n ELSE s0 * power(1 + r2, n) + 12 * m2 * (power(1 + r2, n) - 1) / r2 END AS balance,
        -- the pot that funds the gap between spending and other income at this age, in that year's dollars
        GREATEST(spend - other, 0) * power(1 + infl, n) / NULLIF(swr, 0) AS needed,
        GREATEST(spend - other, 0) * power(1 + infl, n) AS gap_then
      FROM g
    )
    SELECT scenario, n, age, year, ROUND(balance, 2) AS balance, ROUND(needed, 2) AS needed, ROUND(gap_then, 2) AS gap_then,
      balance >= needed AS covered
    FROM proj ORDER BY scenario, n
  3. 03Your savings vs what you need
    SELECT year, age, ROUND(balance) AS your_savings, ROUND(needed) AS nest_egg_needed
    FROM input_1 WHERE scenario = 'plan' ORDER BY n
  4. 04When you can retire
    WITH first AS (
      SELECT scenario, MIN(age) AS retire_age, MIN(year) AS retire_year FROM input_1 WHERE covered AND n >= 1 GROUP BY scenario
    ),
    plan AS (
      SELECT f.retire_age, f.retire_year, p.age0,
        (SELECT balance FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS bal_then,
        (SELECT needed FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS need_then,
        (SELECT gap_then FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS gap_then,
        (SELECT balance FROM input_1 x WHERE x.scenario = 'plan' AND x.age = 65) AS bal_65,
        (SELECT needed FROM input_1 x WHERE x.scenario = 'plan' AND x.age = 65) AS need_65,
        (SELECT MAX(age) FROM input_1 x WHERE x.scenario = 'plan') AS max_age
      FROM (SELECT MIN(age) AS age0 FROM input_1) p LEFT JOIN first f ON f.scenario = 'plan'
    ),
    alt AS (
      SELECT
        (SELECT retire_age FROM first WHERE scenario = 'save_500_more') AS age_save,
        (SELECT retire_age FROM first WHERE scenario = 'earn_1pct_more') AS age_earn
    )
    SELECT metric, amount, unit FROM (
      SELECT 1 AS o, 'You can retire at' AS metric, retire_age AS amount, CASE WHEN retire_age IS NULL THEN 'not by 95 on these numbers' ELSE 'years old' END AS unit FROM plan
      UNION ALL SELECT 2, 'That is the year', retire_year, '' FROM plan
      UNION ALL SELECT 3, 'Years from now', retire_age - age0, 'years' FROM plan
      UNION ALL SELECT 4, 'Nest egg you need then', ROUND(need_then), 'USD' FROM plan
      UNION ALL SELECT 5, 'Savings you will have then', ROUND(bal_then), 'USD' FROM plan
      UNION ALL SELECT 6, 'Savings at 65', ROUND(bal_65), 'USD' FROM plan
    ) ORDER BY o
  5. 05The punch lines
    WITH first AS (
      SELECT scenario, MIN(age) AS retire_age, MIN(year) AS retire_year FROM input_1 WHERE covered AND n >= 1 GROUP BY scenario
    ),
    plan AS (
      SELECT f.retire_age, f.retire_year, p.age0,
        (SELECT balance FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS bal_then,
        (SELECT needed FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS need_then,
        (SELECT gap_then FROM input_1 x WHERE x.scenario = 'plan' AND x.age = f.retire_age) AS gap_then,
        (SELECT balance FROM input_1 x WHERE x.scenario = 'plan' AND x.age = 65) AS bal_65,
        (SELECT needed FROM input_1 x WHERE x.scenario = 'plan' AND x.age = 65) AS need_65,
        (SELECT MAX(age) FROM input_1 x WHERE x.scenario = 'plan') AS max_age
      FROM (SELECT MIN(age) AS age0 FROM input_1) p LEFT JOIN first f ON f.scenario = 'plan'
    ),
    alt AS (
      SELECT
        (SELECT retire_age FROM first WHERE scenario = 'save_500_more') AS age_save,
        (SELECT retire_age FROM first WHERE scenario = 'earn_1pct_more') AS age_earn
    )
    SELECT area, finding, next_step FROM (
      SELECT 1 AS o, 'VERDICT' AS area,
        CASE WHEN retire_age IS NULL THEN '▼ NOT BY 95 ON THESE NUMBERS — your savings never catch the nest egg you need'
             WHEN retire_age <= 55 THEN '▲ YOU CAN RETIRE AT ' || CAST(retire_age AS INTEGER) || ' — IN ' || CAST(retire_year AS INTEGER) || ', ' || CAST(retire_age - age0 AS INTEGER) || ' YEARS FROM NOW'
             WHEN retire_age <= 67 THEN '▲ YOU CAN RETIRE AT ' || CAST(retire_age AS INTEGER) || ' — IN ' || CAST(retire_year AS INTEGER) || ', ' || CAST(retire_age - age0 AS INTEGER) || ' YEARS FROM NOW'
             ELSE '▼ NOT UNTIL ' || CAST(retire_age AS INTEGER) || ' — IN ' || CAST(retire_year AS INTEGER) || ', ' || CAST(retire_age - age0 AS INTEGER) || ' YEARS FROM NOW' END AS finding,
        '' AS next_step FROM plan
      UNION ALL SELECT 2, 'The nest egg you need', CASE WHEN retire_age IS NULL THEN CASE WHEN abs(need_65) >= 1000000 THEN '$' || printf('%.1f', (need_65) / 1000000.0) || 'M' ELSE '$' || printf('%,.0f', need_65) END || ' at 65' ELSE CASE WHEN abs(need_then) >= 1000000 THEN '$' || printf('%.1f', (need_then) / 1000000.0) || 'M' ELSE '$' || printf('%,.0f', need_then) END || ' at ' || CAST(retire_age AS INTEGER) END,
        'Spending minus other income, grown with inflation, divided by the withdrawal rate' FROM plan
      UNION ALL SELECT 3, 'What you will have', CASE WHEN retire_age IS NULL THEN CASE WHEN abs(bal_65) >= 1000000 THEN '$' || printf('%.1f', (bal_65) / 1000000.0) || 'M' ELSE '$' || printf('%,.0f', bal_65) END || ' at 65' ELSE CASE WHEN abs(bal_then) >= 1000000 THEN '$' || printf('%.1f', (bal_then) / 1000000.0) || 'M' ELSE '$' || printf('%,.0f', bal_then) END || ' at ' || CAST(retire_age AS INTEGER) END,
        'Today''s savings plus your monthly contributions, compounded at the expected return' FROM plan
      UNION ALL SELECT 4, 'Save $500 more a month', CASE WHEN a.age_save IS NULL THEN 'still not by 95' ELSE 'retire at ' || CAST(a.age_save AS INTEGER) || CASE WHEN p.retire_age IS NOT NULL AND p.retire_age > a.age_save THEN ' — ' || CAST(p.retire_age - a.age_save AS INTEGER) || ' years earlier' ELSE '' END END,
        'Change "You save per month" above to see the whole curve move' FROM plan p, alt a
      UNION ALL SELECT 5, 'Earn 1% more a year', CASE WHEN a.age_earn IS NULL THEN 'still not by 95' ELSE 'retire at ' || CAST(a.age_earn AS INTEGER) || CASE WHEN p.retire_age IS NOT NULL AND p.retire_age > a.age_earn THEN ' — ' || CAST(p.retire_age - a.age_earn AS INTEGER) || ' years earlier' ELSE '' END END,
        'Fees and allocation decide this number more than anything else' FROM plan p, alt a
      UNION ALL SELECT 6, 'The rule behind it', '4% rule', 'A ' || printf('%.0f', 100 * (SELECT MAX(CASE WHEN assumption = 'Safe withdrawal rate' THEN CAST(value AS DOUBLE) END) / 100.0 FROM input_2)) || '% withdrawal rate means the nest egg is 25x the yearly gap; lower it for a bigger cushion' FROM plan
    ) ORDER BY o

Run this on your books

Free to try — no sign-up, no card. The workflow runs in your browser; your data never leaves your machine.

Make it your own →