DEV Community

Tinashe Nedi
Tinashe Nedi

Posted on

Why I stopped using flat $/kWh to size commercial battery storage.

I've been building energy APIs for about four years. The thing that kept bothering me wasn't the code — it was watching tools I respected give completely wrong BESS cost estimates because they multiplied system size by a flat dollars-per-kilowatt-hour figure.

That's not how battery storage costs work. And it matters a lot when someone is deciding whether to spend $800,000 on a system.

The problem with flat $/kWh

Most BESS calculators I've seen do something like this:


# What most tools do — this is wrong
system_cost = capacity_kwh * cost_per_kwh  # e.g. 500 kWh * $400 = $200,000
Enter fullscreen mode Exit fullscreen mode

The issue is that battery storage has two fundamentally different cost components that scale differently:

  • Energy cost — scales with kWh (how much you can store)
  • Power cost — scales with kW (how fast you can charge/discharge)

A 500 kWh system with a 250 kW inverter has a completely different cost structure than a 500 kWh system with a 500 kW inverter. The flat $/kWh model treats them identically.

The NREL ATB two-component model.

NREL's Annual Technology Baseline (ATB) 2024 separates these correctly:

Total Cost ($/kW) = [BatteryPack ($/kWh) × Duration (h)] + BOS ($/kW)
Enter fullscreen mode Exit fullscreen mode

Where BOS (balance of system) covers the inverter, controls, and
installation — costs that scale with power capacity, not energy capacity.

For LFP at moderate scenario (2024 values from Cole, Ramasamy & Turan 2025, NREL/TP-6A40-93281):

# The correct model
pack_cost_per_kwh = 241   # $/kWh — energy component
bos_cost_per_kw   = 339   # $/kW  — power component
duration_hours    = 4.0   # hours

def calculate_bess_capex(power_kw: float, duration_h: float) -> dict:
    energy_kwh    = power_kw * duration_h
    energy_cost   = energy_kwh * pack_cost_per_kwh
    power_cost    = power_kw * bos_cost_per_kw
    total_cost    = energy_cost + power_cost
    cost_per_kwh  = total_cost / energy_kwh  # effective $/kWh

    return {
        "energy_kwh":     energy_kwh,
        "energy_cost_usd": energy_cost,
        "power_cost_usd":  power_cost,
        "total_cost_usd":  total_cost,
        "effective_per_kwh": cost_per_kwh,  # varies with duration!
    }

# 500 kW, 4-hour system
result = calculate_bess_capex(500, 4.0)
# total: $920,500  |  effective: $460/kWh

# Same energy, 2-hour system (1000 kW inverter)
result2 = calculate_bess_capex(1000, 2.0)  
# total: $1,143,000  |  effective: $571/kWh
Enter fullscreen mode Exit fullscreen mode

Same total energy stored. 24% cost difference. That's the error you make with flat $/kWh.

Why duration changes the effective cost per kWh

Here's the relationship visualized:

Effect of Discharge Duration on LFP Battery Energy Costs

The longer the discharge duration, the lower the effective $/kWh — because you're spreading the fixed power costs (BOS) over more stored energy. This is why 6-8 hour systems often pencil out better for large industrial loads even though the upfront cost is higher.

Round-trip efficiency convention — another source of errors

One more thing that trips people up: manufacturer specs often quote DC-DC round-trip efficiency (0.92–0.95 for LFP). But commercial buildings are billed on AC metered output, so you need the AC-AC figure.

# DC-DC (what manufacturers advertise)
rte_dc_dc = 0.93

# AC-AC (what actually matters for your electricity bill)
rte_ac_ac = 0.85  # LFP — from NREL ATB 2024b and PNNL-33283

# The difference matters for savings calculations
annual_cycles = 365
usable_kwh    = 400

dc_dc_throughput = annual_cycles * usable_kwh * rte_dc_dc  # 136,380 kWh
ac_ac_throughput = annual_cycles * usable_kwh * rte_ac_ac  # 124,100 kWh

# At $0.18/kWh, that's a $2,207/year difference in projected savings
Enter fullscreen mode Exit fullscreen mode

What I built

I wrapped all of this into a FastAPI service that also handles:

  • Load Duration Curve sizing (Neubauer & Simpson 2015, NREL/TP-5400-63162)
  • LCOS calculation per PNNL-33283
  • IRA 2022 §48E ITC stacking (base 30% + up to 20% in adders)
  • Demand charge savings with state-level rates from OpenEI

Every output includes the source equation and the paper it comes from. The API is live on RapidAPI if you want to try it.

GitHub repo |
RapidAPI


Sources: Cole, Ramasamy & Turan (2025) NREL/TP-6A40-93281 ·
Mongird et al. (2022) PNNL-33283 · Neubauer & Simpson (2015)
NREL/TP-5400-63162 · IRA 2022 §48E

Top comments (0)