<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bijayadhs</title>
    <description>The latest articles on DEV Community by Bijayadhs (@bijayadhs).</description>
    <link>https://dev.to/bijayadhs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F475467%2F5fc30afd-ed2e-4369-a498-a7cecad95748.png</url>
      <title>DEV Community: Bijayadhs</title>
      <link>https://dev.to/bijayadhs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bijayadhs"/>
    <language>en</language>
    <item>
      <title>The Notification That Ate My MacBook: A Debugging Story</title>
      <dc:creator>Bijayadhs</dc:creator>
      <pubDate>Sat, 18 Jul 2026 04:21:59 +0000</pubDate>
      <link>https://dev.to/bijayadhs/the-notification-that-ate-my-macbook-a-debugging-story-9fb</link>
      <guid>https://dev.to/bijayadhs/the-notification-that-ate-my-macbook-a-debugging-story-9fb</guid>
      <description>&lt;p&gt;&lt;em&gt;How a deleted-in-the-end time-tracking app pinned a macOS daemon at 99% CPU for who knows how long — and how every "obvious" fix failed before a one-line log command cracked it open.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📖 It started with the wrong question
&lt;/h2&gt;

&lt;p&gt;"My Mac is heating up. What's using all the RAM?"&lt;/p&gt;

&lt;p&gt;That's how it began — a warm laptop, a whirring fan, and the completely reasonable assumption that memory was the villain. It's the question everyone asks, and it's almost always the wrong one.&lt;/p&gt;

&lt;p&gt;Thirty seconds of &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;memory_pressure&lt;/code&gt; later, the RAM theory was dead: &lt;strong&gt;65% free memory, zero swap used&lt;/strong&gt;. Nothing was starving. But sorting processes by CPU instead of memory turned up something strange sitting quietly at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 99.7  36928   635  /usr/sbin/usernoted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;usernoted&lt;/code&gt;. The macOS notification daemon. A background process whose entire job is to pop little banners in the corner of your screen — burning &lt;strong&gt;an entire CPU core, continuously&lt;/strong&gt;, while using a laughable 16 MB of RAM.&lt;/p&gt;

&lt;p&gt;That was the heat. Not Chrome with its forty tabs. Not VS Code. A system daemon nobody has ever consciously interacted with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Note:&lt;/strong&gt; heat is a CPU problem wearing a RAM costume. Fans respond to power draw, and power draw follows CPU cycles. When a machine runs hot, sort by CPU first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🐛 The two-second respawn
&lt;/h2&gt;

&lt;p&gt;The classic move — &lt;code&gt;killall usernoted&lt;/code&gt; — worked for exactly two seconds. The daemon relaunched (they always do; &lt;code&gt;launchd&lt;/code&gt; sees to that) and was back at 97% before the shell prompt returned.&lt;/p&gt;

&lt;p&gt;That respawn told us something important: this wasn't a transient glitch or a stuck request. Something &lt;em&gt;persistent&lt;/em&gt; — data sitting on disk — was feeding the spin. Kill the process all you want; it reloads the same poison on every boot.&lt;/p&gt;

&lt;p&gt;So the question changed from "what process?" to "what data?"&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 Reading the daemon's mind
&lt;/h2&gt;

&lt;p&gt;macOS ships a wonderful and underused tool called &lt;code&gt;sample&lt;/code&gt;, which snapshots a process's call stacks. Pointed at the spinning daemon, it returned the smoking mechanism in beautiful, damning detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-[UNCalendarNotificationTrigger nextTriggerDateAfterDate:withRequestedDate:]
  Calendar.nextDate(after:matching:options:)
    Calendar._enumerateDatesStep(...)
      Calendar.dateAfterMatchingWeekOfYear(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Translation: somewhere in the notification store sat a &lt;strong&gt;scheduled repeating notification&lt;/strong&gt; whose date rule the system could not resolve. Foundation's calendar engine was trying to compute "when does this fire next?" — and looping &lt;em&gt;forever&lt;/em&gt; on the week-of-year component. Not crashing. Not erroring. Just spinning, politely and eternally, at 99%.&lt;/p&gt;

&lt;p&gt;We had the murder weapon. We did not have the murderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  ❌ The graveyard of reasonable theories
&lt;/h2&gt;

&lt;p&gt;Here's where the hunt got humbling. The stack trace screamed "calendar," so we did what any sane person would do — we went after calendar data. One suspect at a time, with a proper measurement after each kill (more on that later):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspect #1: A Google account synced to Calendar.&lt;/strong&gt; Google-synced recurring events with exotic rules are a known source of cursed date math. Removed the account. &lt;em&gt;Still pinned at 96%.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspect #2: Calendar.app's notification toggle.&lt;/strong&gt; Flipped off in System Settings. &lt;em&gt;Still pinned.&lt;/em&gt; (And a lesson hiding inside: the toggle suppresses &lt;em&gt;delivery&lt;/em&gt; — it doesn't unschedule triggers already sitting in the store.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspect #3: A suspect calendar, deleted outright.&lt;/strong&gt; &lt;em&gt;Still pinned.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspects #4–6: Three leftover calendars from old task-manager apps&lt;/strong&gt;, unsubscribed in one sweep. Task and todo apps that "integrate with Apple Calendar" love writing recurring events with alerts, and uninstalling the app leaves the events behind. A genuinely great theory. &lt;em&gt;Still pinned.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suspect #7: The "Work" calendar itself&lt;/strong&gt;, exported to &lt;code&gt;.ics&lt;/code&gt; and grepped for &lt;code&gt;BYWEEKNO&lt;/code&gt; — the one recurrence keyword that maps exactly onto the week-of-year loop in the stack. Ninety-three events, every recurrence rule bounded and boring. &lt;em&gt;Clean.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Meanwhile, every attempt to just &lt;em&gt;read the notification store directly&lt;/em&gt; hit a wall. The legacy database at &lt;code&gt;com.apple.notificationcenter/db2/db&lt;/code&gt;? &lt;strong&gt;Zero bytes&lt;/strong&gt; — deprecated, empty, a decoy that half the internet's fix-it guides still tell you to delete. The real store? Locked behind macOS's privacy walls, unreadable even after granting Full Disk Access, because the Calendar data sits behind its &lt;em&gt;own&lt;/em&gt; separate permission category.&lt;/p&gt;

&lt;p&gt;Six suspects. Six alibis. The daemon spun on, byte-for-byte the same stack trace, as if mocking the entire investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 The one-line command that broke the case
&lt;/h2&gt;

&lt;p&gt;At this point the user — tired of watching calendars get executed for crimes they didn't commit — made a simple suggestion:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Did you try &lt;code&gt;log show --predicate 'process == \"usernoted\"' --info --last 5m&lt;/code&gt;? Check once."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had. Hours earlier. It had returned &lt;em&gt;nothing&lt;/em&gt;, and I'd written the log avenue off.&lt;/p&gt;

&lt;p&gt;Except — it hadn't returned nothing. It had &lt;strong&gt;never run at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Running it this time, without error-suppression, the shell coughed up the truth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;(eval):log:1: too many arguments
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session shell was &lt;strong&gt;zsh&lt;/strong&gt;, and zsh has a &lt;em&gt;builtin&lt;/em&gt; command named &lt;code&gt;log&lt;/code&gt; — an ancient login-watching utility — which had been silently shadowing &lt;code&gt;/usr/bin/log&lt;/code&gt; the entire time. Every one of my earlier log queries had failed instantly, and my tidy little &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; had swallowed the error, converting "command failed" into "logs are empty." I had abandoned the single most direct diagnostic in the toolbox because of a name collision and my own muffler on stderr.&lt;/p&gt;

&lt;p&gt;One character path fix — &lt;code&gt;/usr/bin/log show ...&lt;/code&gt; — and the culprit was on screen in under five seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NotificationRecord app:"com.glimsoft.Timelines"
  req:"Timelines-engagement-longtail-one"
  category:"GSNotificationCategory_Reengagement"
  ...authorizationStatus: Denied...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeated. &lt;strong&gt;Dozens of times per second.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The culprit: a nag that couldn't take no for an answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Timelines&lt;/strong&gt; — a time-tracking app, long forgotten, still sitting in &lt;code&gt;/Applications&lt;/code&gt;, not even running — had at some point scheduled two repeating "re-engagement" notifications. The names say everything: &lt;code&gt;engagement-longtail-one&lt;/code&gt; and &lt;code&gt;engagement-longtail-two&lt;/code&gt;. These are the "hey, come back and use me!" nags that growth teams schedule far into the future.&lt;/p&gt;

&lt;p&gt;And here's the poison pill: at some point, the user had &lt;strong&gt;denied&lt;/strong&gt; the app notification permission. Perfectly normal thing to do. But the scheduled requests didn't get cleaned up — they stayed in the daemon's store, repeating triggers for notifications that could never be shown. So &lt;code&gt;usernoted&lt;/code&gt; sat in an eternal cycle: compute the next fire date (spinning through the pathological week-of-year path), attempt delivery, get refused by the permission system, reschedule, repeat. Forever. On every boot.&lt;/p&gt;

&lt;p&gt;Not a calendar event. Not a Google sync artifact. A &lt;strong&gt;re-engagement notification from an unused app, colliding with a denied permission&lt;/strong&gt;, feeding a genuine infinite-loop bug in Apple's date-matching code.&lt;/p&gt;

&lt;p&gt;The trap in the stack trace, in hindsight: &lt;code&gt;UNCalendarNotificationTrigger&lt;/code&gt; is the API class &lt;em&gt;any&lt;/em&gt; app uses to schedule a date-based local notification. The word "calendar" in the class name refers to calendar &lt;em&gt;math&lt;/em&gt;, not Calendar-the-app. The stack named the mechanism precisely and pointed at the owner not at all — and we spent hours interrogating the app whose name happened to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ The kill, verified
&lt;/h2&gt;

&lt;p&gt;Delete &lt;code&gt;/Applications/Timelines.app&lt;/code&gt;. Restart the daemon. Measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before:  8.0s of CPU time per 8s window   (~99%, sustained, every boot)
after:   0.38s total, zero growth          (idle)
loop frames in stack sample: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fan quiet. Lap cool. Case closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ TL;DR — if your Mac is hot and usernoted (or disnoted) is the culprit
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Confirm it's spinning&lt;/strong&gt; (not a snapshot artifact) by diffing CPU time over a window:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID=$(pgrep -x usernoted); T1=$(ps -o time= -p $PID); sleep 10; ps -o time= -p $PID
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Near-equal = idle. Grew ~10s = pinned.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;See what it's stuck on:&lt;/strong&gt; &lt;code&gt;sample $(pgrep -x usernoted) 3&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ask it who's to blame&lt;/strong&gt; (full path — see the zsh gotcha above):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/log show --predicate 'process == "usernoted"' --info --last 5m
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Look for one bundle ID repeating relentlessly&lt;/strong&gt;, especially a repeating notification with &lt;code&gt;authorizationStatus: Denied&lt;/code&gt;. Delete/fix that app. Done — no reboot, no database wipe.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  📚 What this hunt actually teaches
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Empty output is only evidence if the command actually ran.&lt;/strong&gt; &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; turns failures into false negatives — the most expensive kind, because they close doors that were open. Never suppress stderr on the &lt;em&gt;first&lt;/em&gt; run of a diagnostic. And beware shell builtins shadowing real binaries: &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;stat&lt;/code&gt;, &lt;code&gt;print&lt;/code&gt;. When in doubt, &lt;code&gt;command -v&lt;/code&gt; or the absolute path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stack trace names the mechanism, not the owner.&lt;/strong&gt; Before acting on a class name in a stack, ask: &lt;em&gt;who can instantiate this?&lt;/em&gt; If the answer is "anyone," go find attribution data — logs, records, receipts — before executing suspects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure with counters, not snapshots.&lt;/strong&gt; &lt;code&gt;top -l 1&lt;/code&gt; lied to us mid-hunt, showing 0.0% for a fully pinned process (the first &lt;code&gt;top&lt;/code&gt; sample has no interval to diff against). The measurement that never lies: read the process's cumulative CPU time, sleep N seconds, read it again. A monotonic counter differenced over wall-clock time cannot be fooled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uninstalled ≠ gone, and denied ≠ deleted.&lt;/strong&gt; Apps leave scheduled notifications, calendar events, and login items behind like fingerprints. And denying a permission doesn't purge the requests that permission would have served — it can, apparently, turn them into tiny perpetual-motion machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the meta-lesson:&lt;/strong&gt; the breakthrough came from the person who said &lt;em&gt;"did you actually try the log?"&lt;/em&gt; — re-running a supposedly exhausted avenue and refusing to trust a silent result. The best debugging move of the whole session wasn't a clever command. It was skepticism about a negative.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; if your Mac is hot and &lt;code&gt;usernoted&lt;/code&gt; is at the top of Activity Monitor's CPU tab, don't start by deleting notification databases the internet tells you about — half of them are empty decoys. Start with &lt;code&gt;/usr/bin/log show --predicate 'process == "usernoted"' --last 5m --info&lt;/code&gt; and let the daemon tell you, in its own words, who is wasting its life.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Epilogue: the underlying infinite loop — &lt;code&gt;Calendar.nextDate(matching:)&lt;/code&gt; hanging forever on unsatisfiable components — is Apple's bug; no app data should be able to do that to a system daemon. Reported via Feedback Assistant. The re-engagement notifications that never gave up? Those are on the app. There's something poetic about an app whose parting gift, after being ignored, was to quietly cook the machine it lived on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>debugging</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
