Fuel by Effort, Not by Clock — How Smart Fueling Works in WattLog.pro

Every cyclist knows the advice: eat every 45 minutes. Every cyclist who has done a hard interval session also knows it's wrong. A 60-minute threshold workout burns more carbohydrate than a 90-minute recovery spin, and a timer can't tell the two apart. The moment you notice you should have eaten is the moment it's too late: you're already deep in a set, hands on the bars, and the bonk arrives twenty minutes later.

1 kJ ≈ 1 kcalwork at the pedals ≈ energy from food
400 kJdefault threshold ≈ 30 g of carbs
60–90 g/hwhat the gut can absorb
0.8 L/hupper fluid limit in the plan

I run WattLog.pro, a solo-built training platform with live sensor ingestion, structured workouts that drive a smart trainer in ERG mode, and simulated GPX routes. It already had a "remind me to eat every X minutes" setting, and I kept forgetting to fuel anyway, because the reminder had nothing to do with how hard I was riding. This is how I replaced it with something that does.

Timer vs. what you actually burn

Same rider, three different workouts. An "every 45 minutes" timer treats them by the clock; Smart Fueling treats them by how much carbohydrate actually left the tank:

WorkoutDurationCarbs burned*Timer, every 45 minSmart Fueling**
Recovery, 50% FTP1 h 30 min~70 g2 reminders1 alert
Endurance, 65% FTP2 h~160 g2 reminders2 alerts
Threshold, 90% FTP1 h~155 g1 reminder2 alerts

* Rider with a 250 W FTP, steady riding, using the curve described below (1 kJ ≈ 1 kcal, 4 kcal per gram of carbohydrate). ** Default 400 kJ threshold, assuming you eat at every alert.

An hour at threshold burns as much carbohydrate as two hours of endurance riding and more than twice as much as a 90-minute recovery spin — yet the timer asks you to eat least often during it.

The signal is already on the wire

A power meter or smart trainer reports watts several times a second. Power times time is mechanical work in kilojoules, and cycling has a convenient coincidence. Trained cyclists convert food energy into work at the pedals with an efficiency of roughly 18–26% [1], and one kcal is 4.184 kJ. At about 24% efficiency the two factors cancel:

1 kJ of work at the pedals ÷ 0.24 efficiency ≈ 4.2 kJ of food energy ≈ 1 kcal

It's an approximation: efficiency differs between riders and rises with work rate [1, 2], so the true factor moves a little either way. I cover the maths in more detail in burning kilojoules on the trainer. It was already in my post-ride analytics. It just wasn't available during the ride.

So the core of the engine is an accumulator on the server-side session manager. Every power sample adds trapezoid-integrated work since the last one, with the gap between samples capped at 5 seconds so a Bluetooth dropout can't invent energy. When the total crosses a threshold (400 kJ by default, configurable per athlete), the rider gets told to eat. The state lives in the same Redis-backed session metadata as the rest of a live ride, so a reload or a reconnect to another API worker picks up where it left off instead of resetting the bar to zero.

That first version shipped quickly, and it was already better than the timer. It was also still wrong in an interesting way.

Not all kilojoules are equal

400 kJ at an easy endurance pace and 400 kJ at threshold are the same amount of work, but they don't cost the same fuel. Exercise physiology calls this the "crossover": at low intensity fat is the main fuel, and as intensity rises the body shifts towards carbohydrate, which becomes the dominant fuel at hard efforts [3]. Isotope-tracer studies in cyclists show the same picture in detail: muscle glycogen and blood glucose oxidation rise with every step in intensity, while fat oxidation peaks around moderate work rates and then falls [4, 5]. A bar that counts raw kilojoules under-warns on hard days and nags on easy ones.

The fix is a curve: carbohydrate share of energy as a function of intensity relative to FTP.

Z1Z2Z3Z4Z5Z6 FTP 45% · ×0.69 65% · ×1.00 80% · ×1.23 92% · ×1.42 0%25%50%75%100% 40%60%80%100%120% intensity (% of FTP)
Carbohydrate share of energy (vertical axis) and fuel-bar weight (×) by intensity. The green point is the reference effort: weight 1.0.

The curve is piecewise-linear: roughly 45% at 55% of FTP, 65% at 75%, 80% at 95%, and above 90% beyond FTP. To be clear about where these numbers come from: they are my own approximation, shaped to follow the direction and rough magnitude of the studies above, not values taken from a single paper. Those studies measure intensity against VO2max or maximal workload, not FTP, so the mapping is an engineering estimate. Each sample's kilojoules are then weighted by that share relative to a moderate reference effort:

weight = carb_share(power / FTP) / 0.65   (= 1.0 at a steady moderate effort)

fuel_bar += kJ_delta × weight

Two properties matter here. First, at a moderate effort the weight is exactly 1, so the threshold keeps its meaning and existing settings don't silently change behaviour. Second, without an FTP there's no way to judge intensity, so the bar falls back to unweighted kilojoules rather than guessing. In practice, riding at 96% of FTP now fills the bar almost 1.8 times as fast as riding the same amount of work at 56%. If you don't know your FTP yet, start with an FTP test.

The bar displays grams, not kilojoules. Riders think in "one gel" or "about 30 g", not in kJ, so the threshold is shown as a portion of carbohydrate: 400 kJ × 65% ÷ 4 kcal/g is about 65 g burned, half of which is worth replacing — hence a portion of about 30 g.

When to speak

A correct number is not enough if the alert fires at the wrong moment. Most of the work went into when the app should say something. This is the in-ride fuel bar in its three states:

FuelFuel OK
18 / 30 g carbs
+Portion
FuelEat soon
24 / 30 g · "Eat soon: get ~30 g of carbs ready"
+Portion
FuelFuel low
30 / 30 g · "Eat now: ~30 g carbs"
+Portion

The alerts go through the same channel as the coach's trainer messages: the message panel, a toast on phones, a sound, and spoken audio if voice cues are on. The rest are rules about when not to interrupt:

SituationWhat Smart Fueling does
Bar reaches 75% of the thresholdTurns amber and asks you to get food ready
Bar reaches 100%"Eat now" — repeated every 100 kJ until you do
Threshold crossed above 105% of FTPAlert waits until power drops — the interval isn't interrupted
Route: 0.5 km before a climb of 4%+ (after 1 km+ of flat)"Climb ahead, eat now"
Descent steeper than −5% or climb steeper than 12%Silence — all prompts are held
Tapping "+Portion"Takes one portion off the deficit instead of zeroing it

Holding the alert during a hard effort is a stateful flag set at the crossing, not a live power > 1.05 × FTP check; the naive version flickers the banner on and off with every pedal stroke of a hard interval. And the button started life as "+Gel", which implied gels were the only valid answer. It's now "+Portion": if you're two portions behind and eat once, one is still owed.

On a simulated GPX route the engine scans the elevation profile at start and looks for the last easy moment to eat before the road tilts up:

silencedescent < −5% ≥ 1 km flat climb ≥ 4% Eat here 0.5 km before the climb 0 km2 km4 km6 km8 km10 km
Eat point found from the route profile, and the silent zone on a steep descent (schematic).

The terrain lookahead produced the bug I'm most glad the tests caught. A waypoint crossed inside a blackout zone was skipped but left in the list, and since the trigger condition was "crossed between the previous and current position", it could never fire again: the prompt was silently lost. The fix fires a pending waypoint on the first tick after the blackout ends, and collapses several passed waypoints into one prompt so a stale one never fires late on its own.

Before the ride: what to pack

The other half of fuelling happens at home. Before any session longer than an hour, the setup page shows a card with the carbohydrate and fluid to take along, without connecting a trainer or starting anything:

Pack for this ride

This ride is about 1 h 30 min. Take along about:

Carbohydrates70–90 g
Fluids1.2 L

For a structured workout, the energy comes straight from the plan: each step's target watts times its duration gives exact kilojoules, normalized power gives the intensity, and the same carbohydrate curve is applied per step. A workout of hard surges and easy recoveries needs more carbohydrate than a steady ride at the same average power, and the estimate reflects that. A few examples for a rider with a 250 W FTP on the trainer:

WorkoutEnergyCarbs to packFluids
Endurance 1 h 30 min, 65% FTP878 kJ50–70 g1.2 L
Sweet spot 1 h 30 min, 88% FTP1188 kJ70–90 g1.2 L
Tempo 2 h, 75% FTP1350 kJ90–120 g1.6 L
Endurance 3 h, 65% FTP1755 kJ95–145 g2.4 L

For a GPX route there is no plan, so I use the physics model the trainer simulation already relies on: for every 50 m segment, the power needed to hold the assumed speed against gravity, rolling resistance and air drag, using the rider's weight, bike mass, CdA, Crr and air density. This kind of model is well validated: in road tests it predicted measured power with R² = 0.97 and a standard error of 2.7 W [6]. Descents that need no pedalling count as zero work. A hilly route correctly comes out more expensive than a flat one of the same length.

The numbers then become a carry list: about half of the carbohydrate burned, capped at what the gut can absorb, and shown as a range because a single number would suggest false precision. The caps follow current guidance on carbohydrate intake during exercise: a single carbohydrate source can be oxidised at up to about 60 g/h, and only longer efforts justify about 90 g/h, using multiple transportable carbohydrates such as glucose plus fructose [7]. The same guidance says intake should be adjusted down when the absolute intensity is low, which is what the intensity curve does. The "half of what you burn" factor is my own choice, not a figure from the literature.

Fluids reuse the sweat-rate table from the post-ride debrief. The plan suggests drinking about 80% of the estimated loss, but never more than 0.8 L/h:

Ride intensityIndoorsOutdoors
Low (IF < 0.65)1.0 L/h0.6 L/h
Moderate (IF 0.65–0.85)1.4 L/h0.9 L/h
High (IF > 0.85)1.8 L/h1.2 L/h

Here the honest source is a caveat: sweat rates vary considerably between people, and the position stand on fluid replacement recommends working out your own by weighing yourself before and after a ride, with the goal of keeping fluid loss under about 2% of body weight [8]. The table gives a starting point, not your number. More on that in hydration, electrolytes and salt.

That reuse was deliberate. My first version of the pre-ride card used a flat 500 ml per hour, while the post-ride debrief assumed 1.4 L/h of sweat for a moderate indoor session. The rider would have been told two contradictory things about the same ride. Now all three places work from the same assumptions:

One carbohydrate curve + one sweat-rate table
Before the ride"Pack for this ride" card — energy from the plan or the route model
DuringFuel bar — weighted kilojoules, live
After the rideDebrief — how much you burned and what to eat to recover

A bug the feature found elsewhere

While testing the route card I got a suspicious result: 2.7 L of fluid for a route the page said would take 1 h 41 min. That's 1.6 L/h, double the cap.

Route time (60 km)2.7 L of fluid is…
Shown by the app1 h 41 min1.6 L/h ✗
Real at 18 km/h3 h 22 min0.8 L/h ✓

The fluid maths was right. The duration was wrong, and not in my new code: four route screens had been estimating time as segments × 5 seconds, but segments are 50 m long, so that formula only holds at 36 km/h. At the default 18 km/h every route time in the app had been understated by half. The backend now returns the time with the fuel plan, and all route screens share one distance-over-speed helper.

What it doesn't do

This is guidance, not measurement, and the article would be dishonest without the caveats:

  • The carbohydrate curve is an approximation of average behaviour, and individuals differ a lot. In a study of 300 people, sex, fitness and activity level together explained only 12% of the differences in fat oxidation during exercise; the rest remained unexplained [9]. The app says these are population averages next to every estimate.
  • The route model assumes constant speed. Real riders slow down on climbs, so the model probably overstates the power needed on steep sections.
  • Live weighting needs an FTP and a power source. Without them the bar counts raw work; a heart-rate fallback for the live bar is not built yet.
  • I haven't yet compared the estimates with a real long ride and a scale. The behaviour is pinned down by unit tests, not by field data.

The whole engine can be switched off in Settings, and while it's on it replaces the old fixed-interval eat reminder instead of competing with it.

What I'd take from it

The feature that looked like "show an alert at 400 kJ" turned into three models that have to agree: a live accumulator, a pre-ride plan and a post-ride analysis. Most of the real work was not the formula but making those three tell the rider the same story, and choosing the right moment to speak. The best decision was refusing to let each screen keep its own constants. The most useful habit was treating a number that looked off, like 2.7 L in under two hours, as a bug report rather than a rounding issue.

References

  1. Coyle EF, Sidossis LS, Horowitz JF, Beltz JD. Cycling efficiency is related to the percentage of type I muscle fibers. Med Sci Sports Exerc. 1992;24(7):782–788. PubMed 1501563
  2. Ettema G, Lorås HW. Efficiency in cycling: a review. Eur J Appl Physiol. 2009;106(1):1–14. doi:10.1007/s00421-009-1008-7
  3. Brooks GA, Mercier J. Balance of carbohydrate and lipid utilization during exercise: the "crossover" concept. J Appl Physiol. 1994;76(6):2253–2261. doi:10.1152/jappl.1994.76.6.2253
  4. Romijn JA, Coyle EF, Sidossis LS, et al. Regulation of endogenous fat and carbohydrate metabolism in relation to exercise intensity and duration. Am J Physiol. 1993;265(3):E380–E391. doi:10.1152/ajpendo.1993.265.3.E380
  5. van Loon LJ, Greenhaff PL, Constantin-Teodosiu D, Saris WH, Wagenmakers AJ. The effects of increasing exercise intensity on muscle fuel utilisation in humans. J Physiol. 2001;536(1):295–304. doi:10.1111/j.1469-7793.2001.00295.x
  6. Martin JC, Milliken DL, Cobb JE, McFadden KL, Coggan AR. Validation of a mathematical model for road cycling power. J Appl Biomech. 1998;14(3):276–291. doi:10.1123/jab.14.3.276
  7. Jeukendrup A. A step towards personalized sports nutrition: carbohydrate intake during exercise. Sports Med. 2014;44(Suppl 1):S25–S33. doi:10.1007/s40279-014-0148-z (open access: PMC4008807)
  8. American College of Sports Medicine; Sawka MN, Burke LM, Eichner ER, et al. Exercise and fluid replacement. Med Sci Sports Exerc. 2007;39(2):377–390. doi:10.1249/mss.0b013e31802ca597
  9. Venables MC, Achten J, Jeukendrup AE. Determinants of fat oxidation during exercise in healthy men and women: a cross-sectional study. J Appl Physiol. 2005;98(1):160–167. doi:10.1152/japplphysiol.00662.2003

What did your last ride cost you?

Upload a GPX or TCX file from any ride and see its TSS, power, heart rate and zones — in 30 seconds, no account needed.

Analyze your ride — no account needed →

Upload a GPX or TCX file from Strava or Garmin. How to get one?

← All blog posts