ESP32-P4 vs ESP32-S3: Energy per Token, Measured at the USB Port
📚 More on this topic: LLM on a Microcontroller: ESP32-S3 at 22.6 tok/s · Why Your Local LLM Is Slow · Does RAM Speed Matter for Local LLMs? · InsiderLLM Benchmarks
If you want a small language model running on a battery, tokens per second is the wrong number to stare at. What drains the cell is energy per token. And on a microcontroller the board around the chip can burn as much as the work itself.
So I measured it. The model is the 28.9M-parameter TinyStories model from esp32-ai by slvDev (Viacheslav Sierbov), the one I took from 10.3 to 22.6 tok/s on an ESP32-S3. That figure is end to end with serial output included, or 43.3 ms per token of decode.
The S3 has got faster since. Rebuilding Espressif’s libraries with 64-byte data-cache lines cut decode to 32.7 ms at in-spec clocks and 25.8 ms at an experimental 120 MHz, against 42.5 ms on stock libraries, with identical output (record). Those two builds are S3-B and S3-D here.
The P4 runs the port Barkın Sarıkartal wrote in esp32-ai PR #24, plus four exact changes of mine on top. A logger on each board’s USB line counted every joule for five minutes of back-to-back generation, three times per board.
I wrote the predictions down first and locked them with a hash. Four held. One failed, and I already knew it would. More on that below.
The boxes:
- ESP32-S3 N16R8, run as two builds:
- S3-B: in-spec flash and PSRAM at 80 MHz;
- S3-D: the same chip with experimental 120 MHz memory clocks.
- Waveshare ESP32-P4-ETH: silicon v3.2 at 400 MHz.
All three ran greedy decoding on the same prompt and produced the same 200-token output.
Why energy, not just speed
A faster chip usually draws more power, so speed alone doesn’t tell you what a battery will see. Energy per token folds both together:
energy per token = average power × time per token
A board that draws more power but finishes each token in far less time can come out ahead. A board that idles hungrily pays for that on every token, whether it is computing or not.
There’s a second reason to measure it rather than estimate it. A microcontroller board’s power figure isn’t the SoC alone. The logger here sits on the USB VBUS line. So every number in this piece includes the USB-to-serial bridge, the power LED, the regulators and, on the P4-ETH, an Ethernet PHY that nothing in this test uses. Every figure is board-level. That is what a battery wired to the same board would supply. It isn’t what the SoC alone would use.
The results
Window energy is the headline metric. It’s the energy inside the firmware’s sync windows, which bracket each generation (prefill, decode, sampling and serial output), divided by the tokens produced. Mean of three 5-minute runs per board.
| Board | ms/token | Generate power (per-rep range) | Idle power | Window mJ/token | Total mJ/token | Marginal mJ/token | Tokens per joule (window) |
|---|---|---|---|---|---|---|---|
| ESP32-P4 | 13.572 | 0.5423–0.5431 W | 0.29952 W | 7.909 | 7.9806 | 3.5760 | 126.4 |
| ESP32-S3-D | 26.308 | 0.4468–0.4498 W | 0.20707 W | 12.357 | 12.4297 | 6.6826 | 80.9 |
| ESP32-S3-B | 33.401 | 0.4199–0.4249 W | 0.20408 W | 14.709 | 14.7995 | 7.6562 | 68.0 |
Error bars, from the rep spread plus the reference meter’s accuracy:
| Board | Window mJ/token | Total mJ/token | Marginal mJ/token |
|---|---|---|---|
| P4 | 7.8334–7.9850 | 7.9046–8.0568 | 3.2919–3.8571 |
| S3-D | 12.2148–12.5240 | 12.2885–12.5956 | 6.2688–7.1167 |
| S3-B | 14.4718–14.9191 | 14.5619–15.0104 | 7.0809–8.1985 |
The P4 board uses about 46% less energy per token than the S3-B, from the headline table’s window means (1 − 7.909/14.709). That’s while drawing more power the whole time.
The three metrics answer different questions:
- Total spreads the whole 5-minute window’s energy over the tokens.
- Window counts only the energy inside the generations. It’s almost the same as total here, because the generations run back to back.
- Marginal subtracts what the board draws sitting idle, which I’ll come to.
Faster is cheaper
Look at the two S3 builds first, because they isolate the effect. Same chip, same board, same model, same output. The only difference is the memory clock: 120 MHz on S3-D against 80 MHz on S3-B.
| S3-B | S3-D | |
|---|---|---|
| ms/token | 33.401 | 26.308 |
| Generate power | 0.4199–0.4249 W | 0.4468–0.4498 W |
| Window mJ/token | 14.709 | 12.357 |
S3-D draws more power in every rep and still spends less energy per token, because the time it saves outweighs the extra draw. On this board, the cheapest token is the fastest one.
The P4 is the same story, only stronger. Its board draws 0.5423–0.5431 W while generating, the most of the three. But at 13.572 ms per token it finishes each token in well under half the S3-B’s time, and the energy per token falls to 7.909 mJ.
The practical reading: if you’re picking a part for a battery-powered model, don’t reject the faster chip because its datasheet power is higher. The time per token carries most of the weight.
The idle tax
Here’s the part worth slowing down for. Sitting at its prompt after a fresh reset, doing nothing, each board draws about half of what it draws while generating:
| Board | Idle as a share of generate power | Marginal as a share of total J/token |
|---|---|---|
| ESP32-S3-B | 48% | 52% |
| ESP32-S3-D | 46% | 54% |
| ESP32-P4 | 55% | 45% |
That’s where the two energy numbers split:
- Total J/token is what a battery would see if the board did nothing but generate. The idle draw is part of every token.
- Marginal J/token subtracts the idle draw. It’s the cost of generation on a board that is powered on anyway, say one that is already up to run a sensor or a display.
On marginal, the gap widens: 3.5760 mJ for the P4 against 7.6562 for the S3-B, or 279.64 against 130.63 tokens per joule (the RESULTS “Tokens per joule” table). The P4 gets more out of each extra joule than its board-level total suggests, because more of its total is idle.
That idle floor is the board, not the model. On the P4-ETH it includes an Ethernet PHY that this test never uses. Every regulator and bridge chip on the board takes its share whether you’re generating or not. If you’re building something that runs on a battery, the board you choose, and what you can switch off on it, matters as much as the chip.
Prediction scorecard
I wrote five predictions before any generate run. The file was locked on 2026-10-14 at 16:56 PT, with sha256 b75eb427b6a54b39c2fe750aa76b907ffdb0c00007a6ea0f2500d6f3dadc9e04, and pushed to a private GitHub repo before any generate run. The scoring rule was strict: a prediction holds only if its whole error bar satisfies it.
| Prediction (verbatim) | Result | Verdict | |
|---|---|---|---|
| P1 | “S3-B total energy is 10-25 mJ/token.” | 14.5619–15.0104 mJ | Held |
| P2 | “P4 total J/token is lower than S3-B’s (its 2.3x speed outweighs its higher board power).” | P4 − S3-B = −7.1058 to −6.5051 mJ | Held |
| P3 | “P4 marginal J/token is <= 0.6x S3-B’s.” | ratio 0.4015–0.5447 | Held |
| P4 | “S3-D total J/token is lower than S3-B’s (speed rises more than power).” | S3-D − S3-B = −2.7219 to −1.9664 mJ | Held |
| P5 | “P4 idle power exceeds S3 idle by >= 0.2 W (Ethernet PHY plus the bigger chip).” | +0.0954 W vs S3-B, +0.0925 W vs S3-D | Failed |
What I knew before the lock:
- P5’s outcome. The idle measurements had to run before the lock, so P5’s result was already in hand. I kept it as written rather than drop or reword it. The P4 board does draw more at idle than the S3, but by under 0.1 W, less than half the 0.2 W I predicted: 0.09055–0.10034 W against S3-B, 0.08740–0.09767 W against S3-D.
- The pre-run check figures: 14.37 mJ (S3-B), 12.19 mJ (S3-D) and 7.83 mJ (P4) per token, from short checks of the sync-pin builds before the main runs. A generate window is back-to-back generations, so those figures anticipated P1, P2 and P4.
- No marginal figure. I computed none before the lock, so P3 is the one prediction that was genuinely blind.
How it was measured
No off-the-shelf USB meter could do this. I tried a cheap inline one as a cross-check, and it read 0.00 A on all three boards. It registers a phone load, so its threshold sits somewhere above about 110 mA, and the P4 averaged 108.74 mA while generating.
So I built a logger:
- Hardware: an INA226 current and voltage monitor with a 0.1 Ω shunt, on a perfboard with a Raspberry Pi Pico (RP2040). It sits high side on VBUS, between the PC and the board.
- Sampling: it streams every sample (about 826 Hz) to the PC. The Pico and the PC each integrate the energy independently, and the two must agree within 0.5% or the run fails.
- Calibration: against an Aneng SZ20 multimeter, at five loads from 26 to 146 mA.
- A suspect part: the INA226 behaves like a clone. It reports a genuine ID, but its raw voltage gain read about 5% high and its raw current gain about 6% low, both far outside the real part’s spec. Both channels were linear, so a gain-and-offset fit corrects them. The fit residuals came out at 0.12% or less on current. A recheck at the start of the measurement day read −0.01%.
- The error bars use the multimeter’s published accuracy (±(0.5% + 3 digits) current, ±(0.2% + 10 digits) volts), about ±0.93% on energy at the generate operating points. The fit residuals don’t set the error bars.
- Sync windows: each firmware build raises a GPIO pin for the duration of every generation (GPIO4 on the S3, GPIO6 on the P4). The logger timestamps those edges against its own samples, through an RC filter that removes switching glitches. That’s what lets it split “generating” from “between generations”.
- Same output: the sync-pin builds were checked against their normal builds first. They gave identical greedy output, and ms/token within 1%.
Caveats
- Board-level only. These are USB-port figures for these specific boards. A different S3 or P4 board, with a different regulator, bridge or PHY, would land somewhere else. The idle share in particular is a property of the board.
- One board each. One S3 board and one P4 board, three runs each. Unit-to-unit spread isn’t measured.
- A probable clone sensor. The calibration held through the session, but the INA226’s long-term gain stability isn’t established.
- Marginal is slightly overstated. After a run, idle draw stays above the fresh-reset idle that marginal subtracts: +4 to +6% on S3-B, +5 to +8% on S3-D, +1.4 to +1.8% on the P4. So the true marginal figures are a little lower than shown, most of all on the S3-D.
- This bench runs the S3 about 2% slow. S3-B ran 33.40 ms/token here against 32.7 on its own benchmark, and S3-D 26.31 against 25.8. The P4 ran 13.57 against 13.7, slightly fast. It isn’t the sync pin, since the normal builds show the same offset. I haven’t explained it.
- The cross-check meter couldn’t see the load, as above, so the logger has no independent instrument behind it beyond its calibration.
Sources and try it
- The P4 port: esp32-ai PR #24 by Barkın Sarıkartal, still open upstream.
- My P4 changes on top of it: branch
insiderllm-p4on my fork, pinned ata96ab81. It adds exact dual-core attention, logits in internal SRAM, int4 core weights and the S3 interactive protocol. - The sync-pin build used here:
power-01-sync-gpioat203acd5. - The S3 side: the
insiderllmbranch of the same fork, from the S3 article, offered upstream in esp32-ai issue #26. The S3 sync patch and the full S3 patch stack are in the record below. - The record: the power-01 bench record has the locked pre-registration, the full results, the scoring script, the calibration and per-rep data. The raw sample logs are listed by hash.
- The P4 speed work: six records, starting with the PR #24 reproduction and ending at the interactive build measured here.
Bottom line
If you’re putting a small model on a battery, measure energy per token, and pick for speed. Both comparisons here say the same thing: the build that finishes tokens faster spends less energy on each one, even when it draws more power. The ESP32-P4 board did it at 7.909 mJ per token against 14.709 for the in-spec S3.
Then look at the board, not just the chip. Half of each board’s generating draw is there at idle. If your project leaves the board powered between generations, the fastest chip on the leanest board is the one that wins.
A weekly email with every new guide and measured benchmark.
Subscribe — free, no spam