DEV Community

Cover image for I Spent 26% of the Cycles and 45% of the Capacity
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

I Spent 26% of the Cycles and 45% of the Capacity

I knew two things about my laptop's battery. System information said "264 cycles", and right beside it, "Condition: Normal". Both are true. Both misled me.

My intention this morning was modest: the machine is twenty months old, I would glance at the battery and move on. I saw the summary, felt reassured, and was about to close the window. Before I did, I looked at the raw counters — and the reassurance ended there, because the ledger the battery keeps for itself does not say the same thing as the summary it shows me.

The short version: I have spent a quarter of my cycle budget and nearly half of my capacity budget. The gap between those two numbers is the thing no number on screen ever mentions.

The Three Numbers on Screen

The machine is a 16-inch MacBook Pro, M4 Pro, running macOS 26.6.2 — model identifier Mac16,7, which in Apple's model list is the 16-inch 2024 machine. According to the setup marker it was first booted on 20 January 2025; today it is 618 days old, so twenty months.

Here is the summary from system_profiler SPPowerDataType:

Health Information:
    Cycle Count: 264
    Condition: Normal
    Maximum Capacity: %92
Charge Information:
    Fully Charged: Yes
    State of Charge (%): 100
Enter fullscreen mode Exit fullscreen mode

This is where everyone looks. The Settings app shows the same three things: cycle count, condition, maximum capacity. Ninety-two percent after twenty months looks fine, and "Normal" is a reassuring word.

Apple's definition of maximum capacity is plain: the battery's capacity "relative to when it was new". So it is a ratio. You cannot see its numerator or its denominator, only the result.

Four Numbers, Two Scales

To see the numerator and the denominator you have to read ioreg -rn AppleSmartBattery. There you find not one percentage but a handful of milliamp-hour values that do not resemble each other:

Field Value
DesignCapacity 8,579 mAh
NominalChargeCapacity 7,808 mAh
AppleRawMaxCapacity 7,564 mAh
AppleRawCurrentCapacity 7,531 mAh
PackReserve 244 mAh
StateOfCharge 100
CycleCount 264

Two things corrected themselves at once when I read these lines.

First: the 92% on screen is 7,808 over 8,579. Do the arithmetic and you get 91.01%. The screen rounds up. A single point changes nobody's life, but that was the moment I first understood that the number I am shown is not a raw measurement — it is a processed summary.

Second — and this turned out to be the most expensive lesson in the piece: these numbers are not on one scale.

On my first pass I took the 7,531 mAh sitting in the battery while StateOfCharge read 100, divided it by 8,579, got 87.78%, and wrote that "when the screen says 100%, the battery is really carrying 88% of what it held when new". That was wrong. I tripped over the very question I had just asked: which denominator?

Because the denominator for AppleRawCurrentCapacity is not DesignCapacity — it is AppleRawMaxCapacity, sitting one line away. The ratio is 7,531 / 7,564 = 99.56%. On its own scale the battery really is full, and the screen is telling the truth.

What links the two scales is in the same output:

AppleRawMaxCapacity  7564
PackReserve           244
                     ----
NominalChargeCapacity 7808
Enter fullscreen mode Exit fullscreen mode

The addition closes exactly. The "raw" scale is the one that leaves out a 244 mAh reserve never shown to the user; the "nominal" scale is the one that includes it. Dividing one by the other is adding metres to inches.

The correct sentence is this: what the battery lost over twenty months is the gap between NominalChargeCapacity and DesignCapacity — 8,579 down to 7,808, which is 91.01%. The "100% charged" indicator is not hiding that loss; it simply is not measuring it.

Apple documents none of these IOKit fields — they are not a public API contract, they are properties read from inside the system. But their units are consistent and their sums close, which is why I use them.

Diagram

There is also this: mAh is not a unit of energy. It means nothing until you multiply by voltage. Apple lists the 16-inch 2024 MacBook Pro with a 100 watt-hour battery (99.6 Wh to be exact); my 8,579 mAh only lands on that figure once you bring the pack voltage into it. Asked on its own, "how many mAh?" gives an answer that also says nothing on its own — which is why half of every battery forum is chasing its own tail.

Cycles Are the Wrong Meter

What really puzzled me were the ratios. Let me put the two budgets side by side.

By Apple's own table, my model — MacBook Pro (14-inch and 16-inch, 2024) — has a maximum cycle count of 1,000. The machine says the same thing: the ioreg output carries a DesignCycleCount9C field reading 1000, so I did not have to carry the number over from the document by hand. And the battery is designed to retain "up to 80% of its original charge capacity at its maximum cycle count".

That gives two budgets:

  • Cycle budget: 1,000 cycles. I am at 264 → 26.4%.
  • Capacity budget: from 100% down to 80%, so 20 points. I have lost 8.99 points → 44.9%.

I have spent a quarter of my cycles and nearly half of my capacity allowance.

I stopped and reread that sentence before writing it, because it is easy to jump from here to the wrong conclusion: "so I will never make it to the target." That is not what I am saying, and it is not something I could say. Apple's wording is not a linear promise; it says "up to 80% at its maximum cycle count", not "two points per hundred cycles". In lithium-ion cells, losing capacity quickly early and slowly later is well-known behaviour. A twenty-month-old battery eating its budget front-loaded may be perfectly normal.

What I am claiming is narrower and sturdier: the cycle counter does not explain all of the loss. And I do not have to infer that, because Apple writes it down — a battery's lifespan depends on its "chemical age", which is affected by "factors such as its temperature history and charging pattern". Temperature history is not counted in cycles; it runs on the calendar. So beside the 264 I can see, a second counter is turning that I cannot, and that one does not stop when the machine is closed.

Black boxes spend their most punishing years without filing a report.

I Counted the Last Seven Days: 27 a Month

Everything so far is the battery's own testimony. I also wanted to measure what I do, because the "264 cycles over 20 months" average works out to 13 cycles a month, and that struck me as low.

The macOS power log helps. pmset -g log keeps a bounded window; mine ran from 23 September 17:46 to 30 September 07:58, which is 6.59 days. Every line records the charge percentage at that moment. I read the lines in order and summed only the decreases.

The method is not something I invented; it is exactly Apple's definition of a cycle. A charge cycle happens "when you use all of the battery's power", and that "doesn't necessarily mean a single charge". A cycle is accumulated discharge divided by a hundred.

Here is the script behind that number — it runs as-is on your own machine:

pmset -g log > /tmp/pmlog.txt
python3 - /tmp/pmlog.txt <<'EOF'
import re, sys, datetime, collections

pattern = re.compile(
    r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \+\d{4}'
    r'.*?Using (AC|Batt)\s*\(?Charge:\s*(\d+)', re.I)

events = []
for line in open(sys.argv[1], errors='ignore'):
    e = pattern.match(line)
    if not e:
        continue
    t = datetime.datetime.strptime(e.group(1), '%Y-%m-%d %H:%M:%S')
    record = (t, e.group(2).lower(), int(e.group(3)))
    if events and events[-1][0] == t and events[-1][2] == record[2]:
        continue                      # repeat within the same second
    events.append(record)

drop = 0
step = collections.Counter()
daily = collections.defaultdict(int)
for (t1, _, c1), (t2, _, c2) in zip(events, events[1:]):
    if c2 < c1:                       # decreases ONLY
        drop += c1 - c2
        step[c1 - c2] += 1
        daily[t1.date()] += c1 - c2

days = (events[-1][0] - events[0][0]).total_seconds() / 86400
print('window: %.2f days (%s -> %s)' % (days, events[0][0], events[-1][0]))
print('total decrease: %d points' % drop)
print('cycle equivalent: %.2f' % (drop / 100))
print('monthly rate: %.1f cycles/month' % (drop / 100 / days * 30.4))
print('single-point steps: %d (noise check)' % step[1])
for g in sorted(daily):
    print('  %s  %d points' % (g, daily[g]))
EOF
Enter fullscreen mode Exit fullscreen mode

My output:

window: 6.59 days (2026-09-23 17:46:26 -> 2026-09-30 07:58:08)
total decrease: 589 points
cycle equivalent: 5.89
monthly rate: 27.2 cycles/month
single-point steps: 3 (noise check)
  2026-09-23  75 points
  2026-09-24  66 points
  2026-09-25  70 points
  2026-09-26  97 points
  2026-09-27  99 points
  2026-09-28  86 points
  2026-09-29  96 points
Enter fullscreen mode Exit fullscreen mode

The result:

Measurement Value
Window 6.59 days
Total charge decrease 589 points
Cycle equivalent 5.89
Converted to monthly 27.2 cycles/month
Twenty-month average 13.0 cycles/month

The past seven days ran at more than twice my lifetime average.

The closing lines of the output give it day by day. Laid out as a table, you can see why the total came out so even:

Day Decrease (points)
23 September 75
24 September 66
25 September 70
26 September 97
27 September 99
28 September 86
29 September 96

Watch the two edges of this table: 23 September is not a full day — the log only begins at 17:46, so those 75 points belong to a six-hour slice, the busiest stretch of the week. And 30 September does not appear at all, because not one decrease was written in the eight hours up to 07:58 (the machine was plugged in).

Five of the seven are above ninety, and the lowest day is 66. There is no single exceptional bad day; the load is spread across all of them. That is a long way from the picture in my head of "I run on battery now and then".

To be sure the number was not noise I looked at the distribution of the decreases: 586 of the 589 points come from steps of two points or more, and there are only three single-point steps. This is not a phantom total built from a percentage indicator wobbling up and down; the battery really did discharge.

Let me state the method's two limits as well.

First: the log does not sample continuously, it writes when something happens. I cannot see the dips between events, only the endpoints. That biases the total downward rather than upward — so 5.89 is probably a floor.

Second, and more important: 27.2 is not my "normal rate". That figure comes from multiplying a single week out to a month, and that week was not an ordinary one — as I am about to describe, the machine was running build jobs during those days. If you run this script on your own machine, watch for the same trap: whatever week the window happens to cover, the result tells that week's story. What you want to compare it against is your lifetime average, and the divergence between the two is the signal you are looking for.

That Night: 100% at 20:57, 1% at 00:23

One sequence in the log stood apart from the rest.

09-27 20:57  batt  100
09-28 00:23  batt    1
09-28 08:02  ac      3
Enter fullscreen mode Exit fullscreen mode

At 20:57 on 27 September the battery was full and the machine was running on it. By 00:23 it was at 1%. There is no reading in between, so I do not know the shape of the curve; I know two endpoints. It then stayed there until 08:02 the next morning, when it was plugged in and started recovering from 3%.

In a single night, very nearly a full cycle.

I did not remember what happened that evening, but another ledger did. In the closing days of September my own continuous integration jobs were running on this laptop — a Linux virtual machine on the machine, with one build slot per container inside it. The supervisor's log for 27 September records slots coming up one after another between 20:43 and 21:07. So in the very hours the battery began falling from 100%, the machine was a four-slot build farm.

And the log goes quiet after 00:08 until 08:03 the next morning. The machine simply sat there at 1%.

Two separate records corroborating each other. Neither the battery counter nor the supervisor log could have told this story alone.

The Two Extremes the Battery Remembers

The raw output also holds two lifetime extremes the battery keeps for itself, and they described that night to me in a different language.

The pack is made of three cells. At the moment of this reading, their voltages were 4,287, 4,285 and 4,286 millivolts; they sum to 12,858 mV, and the pack voltage also read 12,858. Half an hour later both had slipped to 12,857 — the numbers are live, but they keep agreeing to within 1 mV. I did not have to guess the cell count; the addition confirmed it.

The extremes the battery remembers:

Field Pack Per cell
MinimumPackVoltage 9,125 mV ~3.04 V
MaximumPackVoltage 13,158 mV ~4.39 V
Right now, "fully charged" 12,858 mV ~4.29 V

The lower extreme is what those 1% readings look like in volts. I cannot say which night it belongs to — these are lifetime extremes and they carry no date. But I know the battery has been down to 3.04 volts somewhere, and after seeing the daily breakdown I also know that was not a rare accident.

The upper extreme is the interesting side. Apple does not explain "reducing the time your Mac spends fully charged" in the language of voltage, but the raw ledger gives that time a value: while the battery waits full, the cells wait at 4.29 volts. Lowering the charge limit means, in practice, lowering that waiting voltage.

None of these lines come from an interface Apple documents; they are fields read from inside the system. Their units are consistent and their sums check out, which is why I use them — but I would not be surprised if the names changed tomorrow.

"It Lives on the Charger" Was an Unmeasured Belief

The sentence in my head when I started this piece was: "my laptop is plugged in all the time anyway, the battery is barely used." Every measurement said the opposite.

In the same 6.59-day window the power source changed 47 times. The charge got as low as 1. A discharge total of 5.89 cycle-equivalents a week does not sit comfortably next to "it lives on the charger".

I would like to give you the time spent on AC versus on battery separately, and I cannot. I tried, and I did not trust the result: while the machine sleeps the log sometimes records the source as "Batt" and, seconds later for the same event, as "AC". Since I cannot separate those two, I am not publishing a ratio — I would rather leave an incomplete table than hand you a wrong number. The discharge total speaks clearly enough without it.

The present state is more entertaining still. While plugged in, pmset -g reports a sleep timer of zero and lists the reason in parentheses: five processes preventing sleep — sharingd, coreaudiod, Claude, Google Chrome, powerd. A browser, an audio server, file sharing, and the agent helping me write these lines.

I wrote this same list down three days ago, and that day it held two names: powerd and Claude. Three days, and it has more than doubled. Nobody adds to that queue on purpose; each process does its own job and pushes sleep a little further away.

Knowing your own machine is less about what it shows you than about what it records. The day I counted 76 wake-ups in seven days from this same log went much the same way: the figure did not match the story I remembered. That piece counted the wake lines of the pmset log; this one sums its charge column — two columns of the same ledger, two separate misconceptions. The gap between the disk space I had promised and the space actually left was another. What all three share is that the summary the device offers is not wrong — it is partial.

Apple's Two Levers, and the Side That Doesn't Fit Me

Apple provides two mechanisms here, and both rest on the same idea.

Optimized Battery Charging (macOS Big Sur 11 and later) uses on-device machine learning to learn your daily charging routine and delays charging past 80% in certain situations.

Charge Limit is newer: on Apple silicon Macs running macOS Tahoe 26.4 or later you can set a ceiling, between 80% and 100%, on "what your Mac considers a full charge". I am on 26.6.2, so this lever is available to me.

The path is in the document too: Apple menu → System Settings → Battery in the sidebar → the info button next to Charging → pick a limit → Done. The Optimized Battery Charging switch lives in the same panel.

The rationale for both is one sentence: reducing the time your Mac spends fully charged improves battery lifespan. Apple also notes that with either feature the machine will occasionally charge to 100% anyway, to keep its state-of-charge estimates accurate.

Now the honest part. Neither lever is a direct remedy for the problem these measurements point at.

The problem in my file is not "the battery waits full"; it is deep discharge. Capping at 80% would shorten runtime in a usage pattern that already reaches 1%, and would bring that night on sooner. So the setting that looks obviously correct could work against me on my own measurements.

So should I do nothing at all? No. Setting the ceiling to 90% rather than 80% looks like a reasonable middle to me: it lowers the voltage the pack waits at without costing as much runtime as 80% would. That is my trade-off, not Apple's advice — Apple gives you the range and leaves the placement to you.

Still, what the measurement actually points at is not a setting but a distribution of work: moving heavy, sustained load off the machine. I had already done that — on 28 September I took those build jobs off the laptop and moved them to a separate machine. I did not make that decision for the battery's sake; looking at it now, the battery wanted the same thing.

When you turn a setting on, the bill is usually kept in a different ledger. Nobody reminds you to open that one.

A Five-Minute Check on Your Own Machine

If you want to repeat this on your own machine, four commands are enough.

See the summary:

system_profiler SPPowerDataType | head -30
Enter fullscreen mode Exit fullscreen mode

See the raw counters — this is where the real table lives:

ioreg -rn AppleSmartBattery -w0 | tr ',' '\n' \
  | grep -E 'DesignCapacity|NominalChargeCapacity|AppleRawCurrentCapacity|CycleCount'
Enter fullscreen mode Exit fullscreen mode

Find out how old the machine is, so you can work out your cycle rate:

stat -f '%SB' /var/db/.AppleSetupDone
Enter fullscreen mode Exit fullscreen mode

See the real discharge of the past week — glance at the raw lines:

pmset -g log | grep -E 'Using (AC|Batt)' | tail -50
Enter fullscreen mode Exit fullscreen mode

To turn it into a number, run the decrease-summing script above as-is; its only dependency is the python3 that ships with the system. Remember to delete /tmp/pmlog.txt afterwards — the script leaves it behind.

Ask three questions while you look. Which denominator does the percentage on screen use? Is your cycle rate the same as your lifetime average, or did the past seven days diverge? And most importantly: does the sentence in your head about how you use this machine match what the log wrote down?

The Fourth Number

What I learned today was not about batteries.

I started with three numbers — 264, Normal, 92% — and all of them were true. What was wrong was not them; it was that I had filled the space between them with a story of my own. "It lives on the charger, it's barely used" was not a measurement. It was an assumption I treated as true because it was comforting, and for twenty months nobody tested it.

The health indicators devices show you are summaries designed to manage you. They are not lying; they are simply saying little. The full ledger is always sitting somewhere. Opening it usually takes one command. Not opening it took me twenty months.

I was not keeping the fourth number. Now I am.

Official Sources

Top comments (0)