This machine was set up in January 2025 — the stamp on /var/db/.AppleSetupDone
says the 20th. My expectations were low on opening
/Library/Logs/DiagnosticReports: the name suggests crash dumps, and crashes are
rare on this machine. The folder had 58 files in it. Application crash reports: zero.
What was inside was something else: the records of two resource budgets my machine
keeps on every process running on it. Fifty-five of them in seven days. Every single
one carried the same line — Action taken: none.
In a moment I'll get to the thing I missed on my first read: there is a process
name at the top of each report, and the budget does not belong to that process. But
let's count what is in the folder first.
What is in the folder
The measurement is from the morning of 11 November 2026. The machine is a MacBook
Pro Mac16,7 with 24 GB of memory, running macOS 26.6.2 (build 25G83) on kernel
xnu-12377.161.14. The whole folder is 10 MB. The reports span eight separate
calendar days but only 6 days 23.8 hours end to end: the earliest violation is
4 November 08:34:06, the latest 11 November 08:21:38. "Seven days" throughout this
piece means that span.
$ ls /Library/Logs/DiagnosticReports | sed 's/.*\.//' | sort | uniq -c
1 DiagnosticLogs
56 diag
1 ips
1 panic
1 Retired
Two of those entries are empty directories (Retired, DiagnosticLogs). The single
.ips file is not a crash report either: its first line reads "bug_type":"298",
making it a jetsam record — a process killed under memory pressure. It is stamped
8 November 06:30, names com.apple.Virtualization.Virtual as the largest process,
and lists all 699 processes alive at that moment. Of the 56 .diag files, 55 are
likewise neither crashes nor panics: they are resource budget records.
The .panic file is its own story and goes in here as a margin note, because I had
no idea it existed. Its first line reads "bug_type":"210", stamped 5 November at
08:57:01. A ResetCounter file written three seconds later says "Reset count: 1". I
asked an independent source when the machine had come up:
$ sysctl -n kern.boottime
{ sec = 1793858196, usec = 877287 } Thu Nov 5 08:56:36 2026
So the panic report was written 25 seconds after boot. That is the expected order,
because macOS writes the panic record on the next boot rather than from the kernel
that died. Counting backwards from the Time Since Boot: 516200s field in one of
the reports from the morning I took the measurement — 11 November 08:19 — lands on
08:56:27, ten seconds off what kern.boottime says and close enough for two
independent methods.
The file's panicString says what happened: Sleep transition timed out after 180
seconds while creating hibernation file. So the machine did not crash under me at
the keyboard; it hung on the way into sleep. My applications not crashing was true
enough — what failed was the machine's own sleep transition. On the morning of
5 November it restarted, filed the paperwork, and I found out six days later while
looking for something else. This article is not about that panic,
but it is a decent reminder of why I opened the folder: records you don't read keep
counting things on your behalf.
Two kinds of event turned up, and the split surprised me:
cpu usage 22
disk writes 33
My first move here was to blame my own command, because my own command was guilty.
The sed 's/.*\.//' above takes everything after the last dot, so it counted all 56
files as diag — but the filenames already separate the two kinds. Twenty-two
reports end in …cpu_resource.diag and every one of them is cpu usage; the other
33 are plain .diag and every one is disk writes. No crossover at all. The folder
didn't mislead me; my one-liner trimming the extension did — and this is only the
first of two places in this piece where my own scanning command produced the
finding.
Two budgets, two opposite policies
Reading the reports turned up two separate limits. On the CPU side what is identical
across all 22 reports is the limit lines; the measurement line changes in every one.
Here is one of them:
Event: cpu usage
CPU: 90 seconds cpu time over 111 seconds (81% cpu average),
exceeding limit of 50% cpu over 180 seconds
CPU limit: 90s
Limit duration: 180s
The numbers on the left move across the 22 reports: the CPU time spent is always
exactly 90 seconds, but the window it fits into runs from 94 to 170 seconds and the
average from 53% to 96%. There are 18 distinct CPU: lines across 22 reports, and
the one above appears in exactly one of them. What never changes is the right-hand
side: exceeding limit of 50% cpu over 180 seconds, CPU limit: 90s,
Limit duration: 180s.
Both numbers sit in the kernel source. Lines 261-262 of osfmk/kern/task_policy.c
read DEFAULT_CPUMON_PERCENTAGE 50 and DEFAULT_CPUMON_INTERVAL (3 * 60). Fifty
per cent, three minutes. Every process that produced a report on my machine sits at
that default, so none of them had raised its ceiling. A process that raised its
ceiling and never crossed it wouldn't appear in this list at all, and this data
can't tell me about those. Where the right to raise it stops is written in the same
file: a process asking for a percentage or an interval above the global cap is
pulled back to the default unless it is cpumon_entitled — and the kernel doesn't
do that quietly, it printfs a "denied attempt to escape CPU monitor" line. One
wanting to switch the monitor off entirely has to present the
com.apple.private.kernel.override-cpumon entitlement. The word private in that
name tells you plainly enough who gets to use it.
The write side is not fixed. Across the 33 reports I found four different limits:
| Limit as printed | Actual value | Reports |
|---|---|---|
| 2147.48 MB | 2 GiB / 24 hours | 21 |
| 8589.93 MB | 8 GiB / 24 hours | 9 |
| 34.36 GB | 32 GiB / 24 hours | 2 |
| 137.44 GB | 128 GiB / 24 hours | 1 |
Each is four times the one above it. That is not a coincidence; it is a rule written
down in the source. In osfmk/kern/coalition.c, the part of coalition_io_rate_exceeded() that runs
after the report has been sent looks like this:
/*
* Continue to monitor the coalition after it hits the initital limit, but increase
* the limit exponentially so that we don't spam the listener.
*/
new_limit = (lei.lei_limit / 1024 / 1024) * 4;
coalition_lock(coalition);
if (new_limit > COALITION_MAX_LOGICAL_WRITES_LIMIT) {
coalition_io_monitor_ctl(coalition, COALITION_IO_LEDGER_DISABLE, -1);
} else {
coalition_io_monitor_ctl(coalition, COALITION_IO_LEDGER_ENABLE, new_limit);
}
The comment states the intent honestly: the point is not to punish, it is to avoid
flooding the listener with notifications. But the consequence is this — whatever
writes heavily to disk gets a budget four times wider after every violation. The
ceiling lives in osfmk/kern/coalition.h as 1ULL << 24 megabytes, which is 16 TiB.
Start at 2 GiB and quadruple: by the seventh violation the limit in force has
reached 8 TiB, the new limit that violation asks for comes to 32 TiB, the ceiling is
overshot and monitoring is switched off altogether. Make enough noise and an app
group earns silence.
What this budget actually counts
I had to understand the number before writing the sentence "it wrote 2 GiB," because
the report does not say written. It says file backed memory dirtied. That is not
bytes landing on the SSD.
The function that feeds the ledger is in osfmk/kern/task.c and it handles four
separate kinds: TASK_WRITE_IMMEDIATE, TASK_WRITE_DEFERRED, TASK_WRITE_METADATA
and TASK_WRITE_INVALIDATED. The first three are added to the account; the fourth —
pages dirtied and then invalidated before reaching disk — is subtracted from it.
So this is not a total, it is a running account that goes up and down. A process that
dirties memory and then abandons the work gets part of its budget back.
The second detail is more practical. The call that updates the group ledger only runs
for writes to internal storage; writes to external storage go to a separate ledger
called logical_writes_to_external and never touch the group budget. Move a heavy job
to an external disk and you become invisible to this budget. That is not a suggested
workaround; it is the reason reading these reports as "here is what my machine writes
most" will always come up short. Work that writes to an external disk never shows up
in this list at all.
The third shows up in the numbers themselves. In 20 of the 33 reports, Writes sits at most 0.02 MB above the limit, and in 26 of them the overshoot stays
caused
under 1 MB: the notice fires the instant the ledger touches the line, so the spill
is close to nothing. Exactly one report makes that pattern unrecognisable:
Command: Autoupdate
Path: /Applications/ChatGPT.app/Contents/Frameworks/Sparkle.framework/…
Writes: 2379.91 MB of file backed memory dirtied over 11 seconds
(216.27 MB per second average), exceeding limit of
24.86 KB per second over 86400 seconds
It spent an entire day's budget in eleven seconds. The ledger caught it 232 MB past
the line; the largest overshoot among the other 32 reports is 6.40 MB. It also took
me a moment to see that the "24.86 KB per second" in that report is the same 2 GiB
budget divided by 86400 seconds, written two lines down in a second unit.
I never once broke the CPU budget
Not one of the 22 CPU reports names an application I installed. The distribution:
7 knowledgeconstructiond
6 spotlightknowledged
3 FPCKService
2 duetexpertd
2 com.apple.Virtualization.VirtualMachine
1 mds_stores
1 apfsd
Nineteen of the paths start under /System/ and three under /usr/libexec. All of
them are macOS's own binaries. The top two — knowledgeconstructiond and
spotlightknowledged — are the background workers of Spotlight and the on-device
knowledge index. Thirteen times in seven days they filled half a core across a
three-minute window and had a report written about it. Because the CPU limit does not
rise after a violation, the same process gets reported again every time. For
knowledgeconstructiond that comes to seven filings in seven days — and the last one
is stamped 08:21 on the morning I took the measurement.
Even the apparent exception, com.apple.Virtualization.VirtualMachine, is Apple's
binary — it just happens to be hosting my virtual machine. So even in the CPU ledger
the binary being named belongs to Apple while the work belongs to me.
The other side inverts. Of the 33 write reports, 12 belong to a named third-party app
group, 6 to a com.apple.* group, and 15 to a group carrying only a number. My
machine tires itself out on CPU; I'm the one tiring out the disk.
The finding: the process on the report doesn't own the budget
On my first read I took the report to mean: a process crossed a limit, the system
noted it. I had it wrong. There are two separate fields at the top, and I had skipped
the second:
Command: git
Path: /opt/homebrew/*/git
Event: disk writes
Writes limit: 2147.48 MB
Resource Coalition: "com.anthropic.claudefordesktop"(17488)
The budget is not git's. It belongs to the coalition git was running inside —
macOS's grouping of an application, its helper processes and its children under a
single resource account. git was only the last straw. The kernel draws that distinction far more clearly in its own log line than the
report does:
kernel: Coalition [64708] caught causing excessive I/O (flavor: 2).
Task I/O: 8192 MB. [Limit : 8192 MB per 86400 secs].
Triggered by process [89137]
"Coalition … caught" and "triggered by process …" sit in the same sentence: the
group is what gets caught, the process is what pulls the trigger. Because the report
file doesn't draw that line in its heading, anyone who reads Command and stops
there knocks on the wrong door.
My data showed this most clearly on a single day. On 5 November, group number 795
produced four reports:
| Violation time | Process on the report | Limit at the time |
|---|---|---|
| 09:02:31 | find |
2 GiB |
| 09:36:41 | qemu-system-x86_64 |
8 GiB |
| 10:10:06 | qemu-system-x86_64 |
32 GiB |
| 18:04:43 | Python |
128 GiB |
Four different binaries, one budget, quadrupling each time. Only the last of the four
resolved the group's name: "com.openai.codex"(795). The other three carry just the
number — I'm inferring they belong to the same group because the number matches; the
reports don't say so themselves. So that day four violations were charged to a coding
agent's account, and three of the reports never mention the agent at all.
Among the named groups, the two heaviest writers are the two AI tools I use every
day: the name com.anthropic.claudefordesktop appears on four reports,
com.openai.codex on two. Count the whole 795 chain above as Codex and the second
figure becomes five — but that is an inference from a number, not something the
reports state. On the Claude side, three of the four reports name something the tool
runs (git, node, ditto) and the fourth names the tool itself (Claude). I had
been asking which thing
wears my disk out most in the setup running on this machine; the answer came back as
"the things working on my behalf." I should have expected that, and I didn't.
What these files say about me
I was curious about the privacy side, because the folder's name makes you cautious.
All 55 reports carry the executable's path, the hardware model and a stack dump;
43 carry a Codesigning ID and 32 a Team ID — seven distinct developer identities
in total. The stack dumps are not small: the 56 .diag files come to 50,447 lines
together.
My first attempt reassured me, and then I saw that what reassured me was my own
command. I searched for my home directory and got zero:
$ grep -l "/Users/mustafaerbay" /Library/Logs/DiagnosticReports/*.diag | wc -l
0
Searching for the folder rather than the username changed the picture: seven reports
contain /Users/. The forms they take are /Users/USER/*/cargo,
/Users/USER/*/rustc, /Users/USER/*/node and /Users/USER/Library/Application.
macOS rewrites the username to a literal USER and intermediate directories to * —
the /opt/homebrew/*/git you saw in the git report above is the same redaction. So
the string I searched for could never have matched; my command produced the "nothing
here" result. The second time in this piece that the same mistake caught me.
What survives the redaction is not nothing. That I run cargo, rustc and node is
readable straight off these files. The panic report carries a stable per-machine
crashReporterKey; the resource records don't. Which document I opened isn't written
here — but 50,000 lines of stack dumps say plenty about which software I run and
when.
The two records don't agree
Having counted the report files, I wanted to test the count with a second method. That
os_log line the kernel writes at the moment of a violation was exactly right for it:
independent of the report file, written straight off the ledger.
/usr/bin/log show --style compact \
--predicate 'eventMessage CONTAINS "caught causing excessive I/O"' \
--start "2026-10-27 00:00:00"
Although I started the query on 27 October, the oldest kernel line I could find in
the unified log was from 9 November 08:18 — I
measured why that window is shorter than I assumed
a while ago. Within the overlapping range the kernel wrote five violation lines. Over
the same range there are four write reports on disk. Three of them pair up.
So two violations exist in the kernel log with no report file, and one report file
exists with no kernel line. Neither record is complete on its own. I'm writing this
down because I have already paid for reading a number as "not on the list, therefore
it didn't happen" on this blog: those 55 reports are not every violation that
occurred, only the ones that became files.
Three places I tripped while measuring
The report's date is not the event's date. Every report carries both a Date/Time
and an End time. Date/Time is the start of the measurement window; the violation
happens at End time. Because the window runs for hours in write reports, the gap
between them reaches 23 hours. I grouped by Date/Time first and the events landed on
the wrong days; worse, I paired a Claude report with the wrong kernel line and produced
a "finding" that the two records disagree by a factor of four at the same instant. No
such finding exists. Matching on End time — 01:47:44 plus 18008 seconds, exactly
06:47:52 — made the two records agree precisely. The stamp in the filename is the
moment the file was written, not the moment of the event either.
My scanning command corrupted the result three times. One of the six lines the
log query returned was not a violation but the record of my own log show command:
the command line carried the text I was searching for as an argument, so it matched
my own search. Together with the two traps above — the sed trimming the extension
and the grep defeated by the USER redaction — that makes three. In all three the
finding came not from the data but from how I looked at it.
The source for my kernel version isn't published. The running kernel is
xnu-12377.161.14; the newest tag in Apple's open source repository is
xnu-12377.121.6. The lines I quoted above come from the published tag — same branch,
but not literally my kernel. The numbers matching the reports on my machine closes that
gap, but I would rather write it down than assume it closed.
The part I couldn't establish
I could not find where the initial 2 GiB limit comes from in published source. The
kernel takes the group budget in from outside: the
COALITION_LEDGER_SET_LOGICAL_WRITES_LIMIT operation in bsd/kern/sys_coalition.c
lands on a coalition_ledger_set_logical_writes_limit(coal, limit) call. So a
user-space component supplies the starting value, and that component isn't open
source. One thing
worth keeping separate: the same repository records a per-process I/O monitor default
of 20480 MB as IOMON_DEFAULT_LIMIT — but that is a different ledger. The numbers in
my reports come from the group ledger, and both the report files and the kernel log say
2048 MB for the starting value. I use the number because two sources agree on it; I am
not claiming it is "the default."
One more thing stayed open. I assumed none of these records leave the machine, and the
settings file both agrees and doesn't.
/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.plist has
AutoSubmit and ThirdPartyDataSubmit set to false — sharing is off. The same
file also carries LastFullSubmissionCalled = 2026-11-10 07:14:43 +0000 and, one
second later, LastFullSubmissionSuccess = 2026-11-10 07:14:44 +0000. I could not establish
from primary sources what that submission carries, so I am not claiming anything went
anywhere. I am only reporting that the file writes both lines.
If you want to look at your own machine
These files are readable by the _analyticsusers group, which means your own user can
read them. Four steps:
-
ls /Library/Logs/DiagnosticReports— how many files, how many are genuine crashes (.ips), how many are resource records (.diag). - In one
.diagfile, read theEvent,Resource CoalitionandAction takenlines. Don't stop atCommand; the budget belongs toResource Coalition. - Note the
Writes limit. If it is larger than 2147.48 MB, that group has violated before and had its limit multiplied; divide by four repeatedly to count how often — assuming the start was 2 GiB. The count belongs to that coalition instance, not to the application's lifetime: on my machinecom.anthropic.claudefordesktopshows up under four separate coalition numbers (773, 11637, 17488, 71862), each starting again at 2147.48 MB. Quitting and relaunching the app resets the counter, andcoalition.crestarts the 24-hour period on every re-arm too. - When grouping by day, use
End time, notDate/Timeand not the filename. - If the group shows only a number — 15 of my 33 write reports do — the one practical
route is matching the kernel log's
Triggered by process [pid]againstpsat the moment it happens. Look later and that pid belongs to somebody else.
The conclusion I drew isn't a settings change. All 55 of my reports say
Action taken: none — but the field being called Action taken says other actions
are possible; task_policy.c calls proc_apply_resource_actions on a CPU violation,
and per-thread limits can raise an exception. None of that fired on my machine. What
changed isn't a switch, it's how I read these files.
There is nothing to blame on the CPU side: my machine is doing its own background work
and measuring it against a half-a-core line over a three-minute window. On the write
side, ownership matters more than the number. A report doesn't say "git wrote too
much," it says "the daily budget of the app group git was running in is spent" — and
that group exists because I started it. I learned that the things wearing my disk out
most are the tools working on my behalf, and I learned it from a record my own machine
keeps. And that record is tuned to grow a little more generous after every
violation, so whatever makes enough noise gradually stops being seen.
I have looked into a limit ledger twice more on this blog in the past month, and
honestly both started somewhere close to here: once on
blowing past CommitLimit with nobody stopping me,
once on
only one of five processes under a single limit getting killed.
The "the limit warns and then does nothing" part was in those pieces too; that isn't
what's new here. Two things are: the owner of the budget is not the process named on
the report, and the budget doesn't shrink when it is broken — it grows. Without the
first you read Command and chase the wrong process; without the second you take a
137 GB limit for a choice Apple made, when that number was written by that group's
own history.
Reading a number without asking whose account it was charged to is from the same
family as looking at an average and missing the stall.
Official Sources
- xnu —
osfmk/kern/coalition.c, group I/O violation and the quadrupling - xnu —
osfmk/kern/coalition.h,COALITION_MAX_LOGICAL_WRITES_LIMIT - xnu —
osfmk/kern/task_policy.c,DEFAULT_CPUMON_PERCENTAGEandDEFAULT_CPUMON_INTERVAL - xnu —
osfmk/kern/task.c,task_update_logical_writesandIOMON_DEFAULT_LIMIT - xnu —
bsd/kern/sys_coalition.c, theCOALITION_LEDGER_SET_LOGICAL_WRITES_LIMITcall path - Apple Support — share analytics information with Apple on Mac
Top comments (0)