ChargeCliff
The math

Why your 30 minute charge took an hour.

Every number on this site comes out of about a dozen equations. This page walks through all of them, plain English first and the actual math second, so you can decide for yourself whether the tool is worth trusting. There's a section near the bottom on what it gets wrong, because a page like this that only lists the confident parts isn't much use to anyone.

The short version

If you read nothing else:

Batteries don't fill at a steady rate

They take a lot of power when they're nearly empty and much less as they fill. The advertised kW number is a brief peak, not a speed.

Cold makes it dramatically worse

Below freezing, some cars cut their intake nearly in half before they'll accept a single amp.

Highway speed and cabin heat eat range

Air resistance climbs with the square of speed, and the cabin heater draws real power that the EPA cycle mostly doesn't capture.

Paying by the minute punishes slow cars

Same plan, same plug, and a slow-charging car can pay four times as much per kWh as a fast one.

Why you can't just divide

Say you drive a Lexus RZ 450e. It holds 64 kWh, you're going from 10% to 80%, so you need about 44.8 kWh. You pull into a 150 kW stall. That's roughly 18 minutes of charging, right?

44.8 kWh150 kW × 60 = 17.9 minutes the tempting arithmetic

It's 31 minutes on a mild day. If it's 30°F out, it's 56 minutes.

The problem is that 150 kW is a ceiling, not a speed. Your car touches that number briefly while the battery is nearly empty, then backs off, and keeps backing off for the rest of the session. By 80% the RZ is pulling around 28 kW. Dividing capacity by charger rating quietly assumes you got the peak the whole time, which is why every tool that does it tells you a number you never actually see.

So instead of dividing once, the model walks the session one percent at a time and adds up how long each step takes at whatever power the car is really pulling right then.

time = Σ 0.01 × pack kWhpower at this % × 60
physics/charging.py · simulate_charge_session()  ·  summed from your starting % to your target

Each vehicle carries its own list of measured breakpoints (at 10% it pulls this much, at 40% this much) and the model reads power straight off that list, interpolating in a straight line between the points. Real curves genuinely step like that, since a BMS derate is an on/off decision rather than a smooth slope, so a handful of honest breakpoints beats a polished curve fit that implies precision nobody measured.

Cold batteries charge slowly

Cold electrolyte is thicker and the cells fight the current harder. Push full power into a cold pack anyway and you start plating metallic lithium onto the anode, which permanently eats capacity and, in the bad cases, eventually shorts the cell. No manufacturer is willing to risk that, so the BMS throttles intake until the pack warms up.

The model treats that as a straight ramp: full derate at freezing, no derate at 68°F, a line in between.

Outside20°F32°F40°F50°F60°F68°F
Power you actually get56%56%66%78%90%100%
multiplier = floorat or below 32°F
multiplier = floor + T 3236 × (1 floor) between 32 and 68°F
multiplier = 1.0at or above 68°F
physics/charging.py · cold_gate_multiplier()  ·  floor defaults to 0.56, set per vehicle

That multiplier gets applied to the curve before anything else, alongside a second one for battery age. Your car takes the lowest of the three ceilings in play: what the stall can push, and what the pack will accept once cold and age are both accounted for.

power = min ( stall rating , curve × cold × age )
physics/charging.py · simulate_charge_session()

One thing this misses: preconditioning. A car that heats its own pack on the way to the charger will beat these numbers, sometimes by a lot. If you've got that feature and you use it, treat the cold columns as a worst case.

When to walk away

The tool reports a number it calls the optimal unplug point, which is just the moment the charger stops being worth your time. It fires when either you're gaining less than a mile of range per minute plugged in, or intake has fallen more than 60% off the peak you saw earlier in the session.

Here's that playing out on the RZ 450e, 10% to 95% on a 150 kW stall on a mild day. Peak was 143 kW:

ChargePullingvs peakYou're gaining
20%143.0 kW100%7.15 mi/min
40%134.3 kW94%6.72 mi/min
50%64.0 kW45%3.20 mi/min
70%60.0 kW42%3.00 mi/min
76%52.8 kW37%2.64 mi/min  ← unplug
80%28.0 kW20%1.40 mi/min
90%14.0 kW10%0.70 mi/min

Notice the drop between 40% and 50%, where the car sheds more than half its intake in ten percentage points. That's the cliff.

Put it in time terms and the case gets blunt. Getting from 10% to 76% takes 27.7 minutes. Carrying on from 76% to 90% costs you another 22 minutes and buys 9 kWh. You'd be spending something like 44% of your total stop on 18% of the energy, which is usually worse than just stopping again later.

Speed, cold, and the heater

The EPA test cycle averages under 50 mph with the climate control mostly idle, which is not what an interstate looks like. Three things stack up against your rated range.

Speed

Air resistance climbs with the square of your speed, so going 75 instead of 55 costs you about 30% more energy per mile. (Drag power technically scales with the cube of speed. It drops back to a square here because we're measuring energy per mile rather than per hour, and dividing by how fast you're covering ground takes one power of v back out.) The model treats roughly a third of your baseline consumption as speed-sensitive drag and leaves the rest flat.

factor = 0.65 + 0.35 × ( your mph55 )2
physics/highway.py · aero_factor()
Speed55 mph65 mph70 mph75 mph80 mph
Energy per mile1.00×1.14×1.22×1.30×1.39×

Cold air

Cold air is denser, so there's more of it to push out of the way, and cold cells are less efficient at delivering what they hold. That's a penalty on top of the speed factor, capped at 25% so a reading of minus ten doesn't produce a silly answer.

The heater

A resistance cabin heater pulls two to three kW, and unlike a gas car you don't get that heat for free off the engine. Spread over distance it works out to:

Wh per mile = heater wattsyour mph
physics/highway.py · hvac_consumption_wh_per_mile()

Which gives you the odd result that the heater costs less per mile the faster you drive. A 3.2 kW heater is 58 Wh/mi at 55 mph and 43 Wh/mi at 75. It draws by the hour while you cover ground by the mile, so getting there sooner means fewer heater-hours on the trip. Speeding up still costs you overall, aerodynamics wins that argument comfortably, just not through this term.

All three at once

Same RZ 450e, same battery, four different days. The safe window is 80% down to 10%, since nobody road trips to zero:

ConditionsUsingFull packRealistic range
55 mph, mild407 Wh/mi157 mi110 mi
75 mph, mild497 Wh/mi129 mi90 mi
75 mph, 45°F533 Wh/mi120 mi84 mi
75 mph, 20°F578 Wh/mi111 mi78 mi

110 miles down to 78 between the best and worst row, on a car that never changed. That spread is what strands people who planned around the window sticker.

Older batteries

You don't need an OBD-II dongle for a decent guess at pack health. Two things wear a battery down: time, as lithium gets locked into a layer that grows on the anode, and use, which the odometer stands in for.

health = 1.0 (0.012 × years) (0.00000125 × miles)
floored at 70%
physics/aging.py · estimate_soh()

A worn pack gets hit twice. It holds less, and its BMS also derates charging power to keep the extra heat from rising internal resistance under control. That second effect is harsher than the first, which is why an old car feels slow at the charger out of proportion to how much range it lost.

Age and milesHealthA 150 kW peak becomes
new100%150.0 kW
5 years, 60,00086.5%122.4 kW
8 years, 120,00075.4%101.0 kW
12 years, 200,00070% (floor)91.0 kW

Losing 13.5% of capacity at the five year mark costs you 18% of your charging speed.

Paying by the minute

Some networks bill by time rather than energy. If your car decides how fast it accepts power and the meter charges by the minute regardless, then a slow car is paying for the privilege of being slow.

Here are two real cars on the same plan at 40 cents a minute, same 10% to 80% window, same mild day, with gas at $3.30:

CarTookGotPaidWorks out to
Ioniq 5 on a 350 kW stall16.1 min51.8 kWh$6.44$0.124/kWh
Bolt EUV on its 55 kW cap54.3 min45.5 kWh$21.71$0.477/kWh
Bolt EUV, to 90%, at 30°F133.3 min52.0 kWh$53.32$1.025/kWh

The Bolt paid 3.8 times as much per kWh as the Ioniq 5 and went home with less energy. The third row is the one worth staring at: $53 for a charge, which works out to the cost of running a car that gets 11.6 miles per gallon. Almost anything with an engine would have been cheaper for that leg.

The tool flags anything at or under $0.40/kWh as fair, up to $0.65 as elevated, and past that as a rate trap.

effective rate = minutes × price per minutekWh you actually got
physics/rate_trap.py · evaluate_rate_trap()

What this gets wrong

All of the above is a model, and a model is a pile of decisions someone made. Here are the ones I'd push back on if I were reading this page instead of writing it.

The heater never turns off

The HVAC number gets applied at every temperature, so a 75 mph run in July still carries about 43 Wh/mi of heater draw it wouldn't have in reality. Warm weather range estimates come out low because of it. That's a real simplification rather than a rounding choice, and it's the one on this list I'd call a flaw instead of a tradeoff.

Baseline consumption is fitted, not measured

Each car's Wh per mile starting point was tuned until the model reproduced published road tests. It isn't a manufacturer figure and it didn't come off a specific car.

Two exponents are educated guesses

The power that health gets raised to, and the split between speed-sensitive and flat consumption, are both numbers that landed close to observed behavior. Neither falls out of first principles, and neither changes per vehicle when it probably should.

Cold gating is a straight line and reality isn't

Real BMS behavior is stepped and specific to the manufacturer. The 56% floor is the middle of a range observed across conservative packs, and preconditioning is ignored completely.

Aging ignores almost everything that matters

Climate, how often the car was DC fast charged, and how long it sat at 100% all drive real degradation. None of them are inputs. The 70% floor is a judgment call rather than a measurement.

The unplug thresholds are opinions

A mile per minute, and 40% off peak, are thresholds I picked because they match when sitting there stops feeling worth it. Someone more patient would pick different ones and wouldn't be wrong.

Most of the charge curves are sourced, not measured

Four of the 42 cars are calibrated against a named published dyno or road test. The other 38 come from public charging reports and get flagged as unverified in both the API and the UI. They're real sourced estimates, but an estimate and a measurement aren't the same thing and the site won't pretend otherwise.

How I check these numbers

Five benchmarks are wired into the test suite. Change a curve or a formula in a way that pushes any of them outside its published range and the build fails, so these aren't claims made once in a README and left to quietly rot.

CarScenarioPublished result
Lexus RZ 300e10→80%, 72°F, 150 kW32 to 37 min
Lexus RZ 450e, cold10→80%, 30°F, 150 kW50 to 60 min
Lexus RZ 450e, highway75 mph, 45°F, heat on115 to 125 mi
Chevy Bolt EUV15→80%, 70°F, 55 kW50 to 55 min
Hyundai Ioniq 510→80%, 72°F, 350 kWabout 18 min

The other 38 cars get a different safeguard. Each one records the date its curve was last checked against real world data, and the build fails if any of them goes 18 months without a look. Manufacturers push charging software updates that genuinely change these curves, so a profile that was right in 2026 won't necessarily still be right later, and I'd rather the test suite catch that than a driver stuck at a charger.

Every figure on this page came out of the same engine that answers /api/v1/calculate/*. None of it was typed in by hand, so when a formula changes these numbers change with it.