salz.run
Method Open notebook · No black box

The math.

Every formula the calculator runs is below. The constants are sourced from peer-reviewed sports-physiology literature where possible and from product labels where it has to be. Nothing is rounded, nothing is hidden.

If you spot an error, please email me — I'll fix it and credit you.

00

Inputs

Seven things you tell the calculator. Everything downstream is derived from these.

Symbol Variable Units Example
BW Body weight kg 75
SR Sweat rate L/hr 1.3
T Session duration hr 5
[Na]sw Sweat sodium profile mg/L 1500
H Heat multiplier × 1.15
CHOhr Carb target g/hr 60
Vb Bottle volume ml 600

Body weight is collected but doesn't feed the formulas directly — it's there so a future version of the calculator can scale carb targets to g/kg/hr. The other six drive everything below.

01

Per-hour losses

For each hour of activity, how much fluid and electrolyte you're shedding into the air and your shirt.

Fluid loss
Fluidloss = SR × 1000 ml/L
Sweat is ~1:1 mass-to-volume with water at body temperature.
Sodium loss
Naloss = SR × [Na]sw × H
Sweat sodium varies 200–2200 mg/L between people. The heat multiplier (1.00 / 1.05 / 1.15 / 1.25) accounts for the upward drift in sodium concentration when you sweat harder in heat.
Potassium loss
Kloss = SR × 200 mg/L
Sweat K is much less variable than Na — typical population mean is 200 mg/L. The calculator uses this as a constant rather than asking you to estimate it.
Magnesium loss
Mgloss = SR × 30 mg/L
Same logic. ~30 mg of Mg per liter of sweat.
02

Replacement targets

You don't want to replace 100% of what you lose. Partial replacement is safer (less GI distress, no hyponatremia risk) and matches how the body actually wants to work — it can pull from its own reserves between bottles. Each loss is multiplied by a fraction:

Fluidtarget = Fluidloss × 0.75
Natarget = Naloss × 0.65
Ktarget = Kloss × 1.00
Mgtarget = Mgloss × 0.5

Fluid at 75% — the upper bound where most athletes can drink without GI revolt. Drinking back 100% of sweat losses is for ultras with very low intensity and very long timelines; at race pace you'll spend more energy peeing than running.

Sodium at 65% — Baker (2017) and the GSSI Sports Science Exchange #153 both land here for endurance work over 2 hours. Higher than this and you risk salty, off-putting drinks that you stop drinking; lower and cramping risk climbs.

Potassium at 100% — K losses are small enough that full replacement is cheap and well-tolerated.

Magnesium at 50% — Mg has a long body half-life and large stores in bone. Acute partial replacement is fine.

03

Bottles per hour

Now we need to know how many bottles you have to drink per hour to hit the fluid target. That's how we'll split the per-hour electrolyte targets across bottles.

Bottleshr = Fluidtarget ÷ Vb
How many bottles of your chosen size you need to finish each hour.
Bottlessession = ceil( Bottleshr × T )
Round up — better to prep one extra than run dry at hour 4.
04

Per-bottle nutrients

Spread each hourly target evenly across the bottles you'll drink that hour.

Nabottle = Natarget ÷ Bottleshr
Kbottle = Ktarget ÷ Bottleshr
Mgbottle = Mgtarget ÷ Bottleshr
CHObottle = CHOhr ÷ Bottleshr
05

Grams of each compound

You don't pour sodium directly — you pour sodium chloride. So every per-bottle mineral target gets divided by the mineral's mass fraction in its source compound.

gNaCl = Nabottle ÷ 393 mg/g
Sodium is 22.99 / 58.44 = 39.3% of table salt by mass → 393 mg Na per gram of NaCl.
gKCl = Kbottle ÷ 524 mg/g
Potassium is 39.10 / 74.55 = 52.4% of potassium chloride → 524 mg K per gram of KCl.
gCalm = Mgbottle ÷ 80 mg/g
Magnesium citrate is roughly 80 mg of elemental Mg per gram of product per the Natural Vitality Calm label. Other Mg-citrate brands differ — check yours.
gdex = CHObottle
Pure dextrose powder is 100% carbohydrate by mass — no conversion needed.

Volume hints in the calculator (≈ pinch / ≈ 1/4 tsp / etc.) use these grams-per-teaspoon approximations for level measurements: salt 5.7 g/tsp · KCl 4.5 g/tsp · Calm 2 g/tsp · dextrose 12 g/tbsp . They're for sanity-checking the scale, not for replacing it.

06

Sanity checks

Two concentration checks the calculator runs against the bottle you're about to mix:

[Na]bottle = (Nabottle ÷ Vb) × 1000
Target: 500–1500 mg/L. Above 1500 mg/L tastes uncomfortably salty and starts to slow gastric emptying. The calculator warns you to split into more bottles. Below 300 mg/L (when your sweat profile is salty) means the dose is too dilute to do its job.
[CHO]bottle = (CHObottle ÷ Vb) × 100%
Target: 4–8% by mass. This is the SGLT1 sweet spot — above 8% (10% for some people) and gastric emptying slows enough that you feel the slosh. Below 4% and you're leaving fuel on the table.
07

Worked example

A 50K trail race for someone roughly like me: 75 kg, sweat rate 1.3 L/hr, salty sweater (1500 mg/L Na profile), hot day (heat factor 1.15), 5 hours of running, 60 g/hr of carbs, 600 ml bottles.

Per-hour losses
Fluidloss 1,300 ml
Naloss 2,243 mg
Kloss 260 mg
Mgloss 39 mg
Per-hour targets (after replacement %)
Fluidtarget 975 ml
Natarget 1,458 mg
Ktarget 260 mg
Mgtarget 19.5 mg
Bottles
Bottleshr 1.63
Bottlessession 9
Per bottle (the actual recipe)
Nabottle 897 mg
Kbottle 160 mg
Mgbottle 12 mg
CHObottle 36.9 g
Powder to weigh
NaCl (table salt) 2.28 g
KCl 0.31 g
Mg citrate (Calm) 0.15 g
Dextrose 36.9 g
Sanity checks
[Na]bottle 1,495 mg/L
[CHO]bottle 6.2%

Both checks pass: sodium at 1,495 mg/L is in the 500–1500 mg/L band, carbs at 6.2% are in the 4–8% SGLT1 zone. 9 bottles to prep, weigh, shake, and pack. Race day, you grab them out of the cooler.

08

Constants reference

Every constant lives in one file. Change one number there and the calculator follows everywhere — this page, the home calculator, the worked example above. No black box.

File src/lib/electrolytes.ts
Constants only
// Mass fractions of active mineral per gram of compound
export const NA_PER_G_NACL = 393; // mg Na per g NaCl  (22.99/58.44 × 1000)
export const K_PER_G_KCL = 524; // mg K per g KCl    (39.10/74.55 × 1000)
export const MG_PER_G_CALM = 80; // mg Mg per g Natural Vitality Calm

// Typical sweat composition
export const K_SWEAT_MGPL = 200;
export const MG_SWEAT_MGPL = 30;

// Replacement ratios (conservative — avoid GI distress / hyponatremia)
export const REPLACE_FLUID = 0.75;
export const REPLACE_NA = 0.65;
export const REPLACE_K = 1.0;
export const REPLACE_MG = 0.5;

// Volume-to-mass approximations (fine-grain powders, level measure)
export const G_PER_TSP_SALT = 5.7;
export const G_PER_TSP_KCL = 4.5;
export const G_PER_TSP_CALM = 2.0;
export const G_PER_TBSP_DEX = 12;

The table below is the human-readable view. If you want to fork the calculator and tune it for your own physiology, this is the file you change.

Constant Value Source
NA_PER_G_NACL 393 mg/g 22.99 / 58.44 (atomic mass)
K_PER_G_KCL 524 mg/g 39.10 / 74.55 (atomic mass)
MG_PER_G_CALM 80 mg/g Natural Vitality Calm label
K_SWEAT_MGPL 200 mg/L Baker 2017 population mean
MG_SWEAT_MGPL 30 mg/L Maughan & Shirreffs sweat panel
REPLACE_FLUID 0.75 ACSM upper bound for endurance
REPLACE_NA 0.65 GSSI SSE #153
REPLACE_K 1.00 Full replacement; tolerated well
REPLACE_MG 0.5 Acute partial; bone reserves
09

The function, in full

Every step on this page, top to bottom, compiled into the function the calculator actually runs. The input interface lists exactly what you tell it; the body is the same sequence of formulas you just read in section 01 through 06.

File src/lib/electrolytes.ts
calculate()
export interface CalcInput {
  bw: number; // kg
  sr: number; // L/hr (= kg/hr)
  dur: number; // hours
  naProfile: number; // mg/L sweat Na
  heatMult: number; // multiplier on Na conc
  carbsPerHr: number; // g/hr
  bottleMl: number;
}

export interface CalcResult {
  bottlesPerHr: number;
  totalBottles: number;
  naPerBottle: number;
  kPerBottle: number;
  mgPerBottle: number;
  choPerBottle: number;
  gSalt: number;
  gKcl: number;
  gMg: number;
  gDex: number;
  fluidPerHrMl: number;
  naPerHrMg: number;
  choPerHrG: number;
  totalFluidL: number;
  totalNaMg: number;
  totalChoG: number;
  naConcMgL: number;
  choConcPct: number;
}

export function calculate(s: CalcInput): CalcResult {
  const fluidLossMl = s.sr * 1000;
  const naLossMg = s.sr * s.naProfile * s.heatMult;
  const kLossMg = s.sr * K_SWEAT_MGPL;
  const mgLossMg = s.sr * MG_SWEAT_MGPL;

  const fluidTargetMl = fluidLossMl * REPLACE_FLUID;
  const naTargetMg = naLossMg * REPLACE_NA;
  const kTargetMg = kLossMg * REPLACE_K;
  const mgTargetMg = mgLossMg * REPLACE_MG;
  const choTargetG = s.carbsPerHr;

  const bottlesPerHr = fluidTargetMl / s.bottleMl;
  const totalBottles = Math.max(1, Math.ceil(bottlesPerHr * s.dur));

  const naPerBottle = naTargetMg / bottlesPerHr;
  const kPerBottle = kTargetMg / bottlesPerHr;
  const mgPerBottle = mgTargetMg / bottlesPerHr;
  const choPerBottle = choTargetG / bottlesPerHr;

  const gSalt = naPerBottle / NA_PER_G_NACL;
  const gKcl = kPerBottle / K_PER_G_KCL;
  const gMg = mgPerBottle / MG_PER_G_CALM;
  const gDex = choPerBottle;

  const naConcMgL = (naPerBottle / s.bottleMl) * 1000;
  const choConcPct = (choPerBottle / s.bottleMl) * 100;

  return {
    bottlesPerHr,
    totalBottles,
    naPerBottle,
    kPerBottle,
    mgPerBottle,
    choPerBottle,
    gSalt,
    gKcl,
    gMg,
    gDex,
    fluidPerHrMl: fluidTargetMl,
    naPerHrMg: naTargetMg,
    choPerHrG: choTargetG,
    totalFluidL: (fluidTargetMl * s.dur) / 1000,
    totalNaMg: naTargetMg * s.dur,
    totalChoG: choTargetG * s.dur,
    naConcMgL,
    choConcPct,
  };
}

That's the whole thing. No remote API call, no hidden multipliers, no second pass — what you see here is what runs in your browser every time you nudge a slider.

Ready to run the numbers?
Plug in your own and skip the algebra.
Open the calculator →