DEV Community

Cover image for The Optimization That Was Too Good: Why Our Push Notifications Only Worked When You Weren't Looking
Dhruv Jani
Dhruv Jani Subscriber

Posted on

The Optimization That Was Too Good: Why Our Push Notifications Only Worked When You Weren't Looking

Summer Bug Smash: Smash Stories 🐛🛹

This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.

When I was building the push notification system for ShelfTalk — a real-time social app that won the GitHub Finish Up A Thon Challenge on DEV — I wanted to be considerate of my users. There's nothing more annoying than actively chatting in a web app and having your desktop ping you with a notification for the exact message you're currently reading.

GitHub logo JaniDhruv / ShelfTalk

A full-stack real-time social architecture featuring Socket.io instant messaging, synchronized reading rooms, and a high-performance React/Vite frontend backed by MongoDB Atlas and GridFS and desktop push notifications for groups and chats.

ShelfTalk Logo

ShelfTalk

Books don't talk. We do.

Turn every reading list into a living conversation.

🌐 Live Demo⚡ Quick Setup✨ Features🛠 Tech Stack

Node.js React MongoDB Socket.io Vercel


📖 What is ShelfTalk?

ShelfTalk is a full-stack social community platform built for book lovers. Think of it as a blend of Goodreads and Discord — readers can share updates, annotate passages, join genre-based clubs, chat in real-time, and coordinate live reading sessions together.

Originally built as a college project, ShelfTalk has since been revived and significantly expanded with real-time infrastructure, a live reading room feature, a personal reading diary, an invite friends feature to help the community grow, cloud file storage, and more.


✨ Core Features

📝 Community Posts

  • Publish reading updates and annotate favourite passages
  • Threaded comments keep every discussion organized
  • Reactions and saves from your personal dashboard

🔍 Powerful Discovery

  • Filter readers by genre, location, and interests
  • Browse vibrant…

So, I added what I thought was a brilliant little optimization:

export const sendPushNotification = (title, options = {}, requireHidden = false) => {
  if (!('Notification' in window)) return;

  if (Notification.permission === 'granted') {
    // If requireHidden is true, only fire if the tab is hidden
    if (requireHidden && !document.hidden) {
      return; // <-- The "Brilliant" Optimization
    }

    try {
      const notification = new Notification(title, options);
      // ...
    } catch (e) {
      console.error(e);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

The logic was simple: check document.hidden (the Page Visibility API). If the user is currently looking at the ShelfTalk tab, suppress the desktop notification. No redundant pings. Clean UX.

I deployed it, patted myself on the back, and went about my day.

🔍 The Mystery

A few days later, the bug reports started rolling in. Users were complaining that they were missing important direct messages and group mentions.

I tested it on my machine. I minimized the browser, had a friend send me a message, and — ding! — the notification popped up perfectly. I opened the window, got another message, and no notification appeared. It was working exactly as designed.

So why were people missing messages?

💡 The "Ah-Ha" Moment

I asked one of the users to describe their setup.

"Oh, I usually leave ShelfTalk open on my second monitor while I work on my main screen."

And suddenly it clicked.

The Page Visibility API (document.hidden) only returns true if the page is completely hidden — either behind other windows in a background tab, or in a minimized window. If the window is visible on a second monitor — even if you are actively working in a completely different app on your main monitor — document.hidden is false.

Because the tab was technically "visible" to the operating system, my code assumed the user was staring directly at it. It silently dropped every notification. The user, focused on their main screen, never heard a ping and missed the message entirely.

Even worse: if a user left ShelfTalk open on their desktop at the office and went home, their home laptop would never notify them either — because the office machine was technically still "looking" at the app.

TL;DR: document.hidden === false does not mean the user is paying attention. It just means the OS can see the window.

🔨 The Smash

Sometimes, the best way to fix a bug is to delete the code you were so proud of writing.

I removed the optimization entirely:

-    // If requireHidden is true, only fire if the tab is hidden
-    if (requireHidden && !document.hidden) {
-      return;
-    }
+    // Notifications will now fire even if the tab is visible,
+    // to ensure they appear across all active machines.
Enter fullscreen mode Exit fullscreen mode

(See the exact commit here)

📖 The Lesson

We often try to be "too smart" with our UX optimizations. I assumed that "window visible" equaled "user paying attention." In the era of ultra-wide monitors, multiple screens, and users being logged in on five different devices at once, that assumption was completely wrong.

By trying to save users from a slightly annoying notification, I ended up breaking the core promise of a notification system: telling you when something happens.

Three things this bug taught me:

  1. Test with real user setups, not just yours. My single-monitor dev machine couldn't reproduce the issue. Multi-monitor setups, tablets propped up on desks, browsers left open on work machines — these are all "visible" to the OS.

  2. document.hidden is about the OS viewport, not human attention. The MDN docs are clear about this, but it's easy to project human meaning onto a boolean that was never designed to carry it.

  3. When in doubt, notify. A slightly redundant ping is annoying. A silently dropped message is a broken product.

Now, ShelfTalk might ping you even if you're looking right at it. But at least you'll never miss a message again. 🔔

Top comments (33)

Collapse
 
bryanw profile image
Bryan Williams

This one hit home because I shipped the same shape of bug this week in a totally different place: a CI classifier that read test results from stdout. "No output" meant "test died mid-run" — except when the process crashed at exit and the output just lost the flush race. Same trap as document.hidden: a signal pressed into carrying a meaning it was never designed for. Visible != attentive, and silent != failed. My fix rhymed with the delivery-vs-seen idea from the comments — move the truth to a channel that survives the failure mode (a synchronously-written result file) instead of inferring it from a side effect. Great writeup, and the second-monitor detail is exactly the kind of real-user setup no dev machine ever reproduces.

Collapse
 
dj29 profile image
Dhruv Jani

That's a really good parallel. The “signal carrying a meaning it was never designed for” part is exactly what got me here too.

And yeah, the second-monitor setup was the kind of real-world case I completely missed while testing. Glad the story resonated!

Collapse
 
p0rt profile image
Sergei Parfenov

the office-machine example makes this a distributed-state bug, not only a visibility bug. each device independently suppresses delivery from local state, but no device has proof that the human saw the message. document.hidden can choose a presentation channel; it should never be allowed to delete the event.

deleting the optimization is the right safe fix. the longer-term state machine is created -> delivered per device -> seen per user, with suppression only after the server has a seen acknowledgement. otherwise one technically visible device vetoes every other device without evidence.

does ShelfTalk already have server-side unread state u could use for that, or is notification state local-only today?

Collapse
 
dj29 profile image
Dhruv Jani

Good catch, and yeah, this is basically @anassBld's distributed-state point taken one step further with an actual engineering answer instead of just "combine signals." You're right that document.hidden can only ever be a presentation-channel decision, not a delete-the-event one — that's exactly the trap I fell into, just framed better than I did in the post 😅

Went back and checked, and turns out I already halfway did what you're describing:

// requireHidden is now dead code, never called with true anywhere
// push fires on every connected device regardless of local visibility
Enter fullscreen mode Exit fullscreen mode

So no device gets veto power anymore, that part's fixed.

To your actual question: it's split today.

  • In-app notifications (the bell/inbox) do have real server-side state — isRead on the Notification model in Mongo, per user, only flipped by an explicit markAsRead call. That's already a legit "seen" signal.
  • Desktop push popups have none of that. They're fire-and-forget over the socket — no delivered record, no seen record, nothing persisted.

So the created → delivered per device → seen per user pipeline you're describing doesn't exist for push yet. If I take this further than a demo project, that's the actual next build, not another local heuristic. Appreciate you laying out the model this clearly.

Collapse
 
p0rt profile image
Sergei Parfenov

this is the right split. isRead gives u a user-level fact, while push is only an attempted delivery until each device acknowledges it. the next useful step is a per-device delivery id that folds into one user-level seen state, so presentation choices never decide whether the event exists.

Collapse
 
ofri-peretz profile image
Ofri Peretz

The document.hidden false positive on multi-monitor setups is a clean example of an API whose name implies a stronger guarantee than it actually delivers — "not hidden" means the OS can render it, not that a human is paying attention. I've run into the same semantic gap in security contexts: document.hasFocus(), Notification.permission === 'granted', and similar presence signals all have different failure modes, and combining them doesn't stack the guarantees the way you'd expect. The delete-the-optimization fix is almost always the right call once you've found the ceiling. One thing worth flagging: LLMs generating notification code tend to reproduce this exact assumption because the MDN prose for document.hidden is technically accurate but elides the multi-monitor nuance entirely — so if you're reviewing AI-assisted PRs in this area, it's worth adding it to the checklist.

Collapse
 
dj29 profile image
Dhruv Jani

Thanks @ofri-peretz! for pointing that out! I hadn't considered the LLM-generated code angle, but you're absolutely right. I'll keep that nuance in mind when reviewing AI-assisted code in this area. Really appreciate the insight!

Collapse
 
ofri-peretz profile image
Ofri Peretz

The LLM angle is worth naming precisely, because the failure mode is consistent: models pattern-match on the common service worker examples that call showNotification() unconditionally, then bolt on foreground detection as an afterthought. The clients.matchAll() check looks right on inspection but often returns an empty array because the page never registered a controllerchange listener — so the worker concludes there's no active client and falls through to the background path every time. You end up with code that passes review, passes manual testing in a fresh tab, and silently does the wrong thing in production. The concede: this isn't unique to AI-generated code, it's just that the subtle async coordination between page and worker is exactly the kind of thing confidence-calibrated generation gets wrong at higher rates.

Thread Thread
 
dj29 profile image
Dhruv Jani

Yeah, this is exactly the nuance I wish I'd included more explicitly in the post. The bug itself definitely isn't “an AI bug” — it's a subtle page ↔ service-worker coordination issue that can survive both review and casual testing.

What AI changes is the probability of that failure mode: the generated code looks completely reasonable because it's assembled from familiar notification/service-worker patterns, while the missing coordination between those pieces is easy to overlook.

That “looks right on inspection, silently fails in the real environment” distinction is probably the biggest lesson I took from this bug. Thanks for articulating it so precisely.

Thread Thread
 
ofri-peretz profile image
Ofri Peretz

The activation race is the specific mechanism worth naming: navigator.serviceWorker.controller is null until the worker reaches the activated state, so any message dispatched before that resolves drops silently — no error, no console warning. In development the race is nearly always pre-won because the worker is already active from a prior session, which masks it completely during manual testing. You're right that this class of bug predates AI tooling entirely; what shifts is that pattern-assembled code tends to get each component correct in isolation while skipping the state-machine coordination between them, so every individual piece looks exactly as it should under review.

Thread Thread
 
dj29 profile image
Dhruv Jani

That activation-race framing is the missing piece — dev masking it because the worker's already warm explains exactly why this kind of bug survives local testing every time. Makes me think the real guard isn't more careful review, it's testing against a cold registration on purpose (hard refresh / unregister first) instead of trusting a warm dev session. Appreciate you naming the actual mechanism instead of leaving it at "coordination issue" — that's the version I'll actually remember next time I touch a service worker.

Collapse
 
alexshev profile image
Alex Shev

The useful test for “The Optimization That Was Too Good: Why Our Push Notifications Only Worked When You Weren't Looking” is whether the lesson changes a team decision, not just a local implementation. I’d capture the failure signal, the guardrail that caught it, and the smallest regression test that keeps it from returning. That turns a good postmortem into something another team can actually reuse.

Collapse
 
dj29 profile image
Dhruv Jani

That's a great way to frame it! In my case, though, ShelfTalk was originally a college project and I was the only one building it. I revived it later for the GitHub Finish-Up-A-Thon challenge, which is when I dug much deeper into these edge cases. Still, love the point about turning a bug into something reusable.

Collapse
 
anasbuilds997 profile image
anassBld

Great debugging story. Your "Ah-Ha" moment is actually a specific instance of a general distributed-systems trap: inferring client state from a single signal.

document.hidden told you about the OS viewport, but you used it to model human attention — and attention was never the thing being measured.

The fix that usually survives multi-monitor reality without fully reverting is to combine signals: fire the notification, but let the client acknowledge receipt (focus event, service-worker interaction) mark it as seen. That way you optimize for "don't annoy an attentive user" based on observed behavior rather than predicted attention — closer to how push systems treat delivery vs. read receipts.

The deeper lesson matches what we keep hitting building agent infrastructure: any state inferred from one observation ("visible", "returned 200") should stay provisional until confirmed by a second independent signal. Congratulations on the Finish-Up-A-Thon win, by the way.

Collapse
 
dj29 profile image
Dhruv Jani

This is a really interesting way to look at it. I was basically treating document.hidden as “the user isn't looking,” when that's obviously not what it actually tells us. 😅

I really like the idea of separating delivery from the “seen” state instead of trying to predict attention. Definitely something I'd implement differently if I were rebuilding this now. Thanks for the detailed response!

Collapse
 
articlefeed profile image
Boris Dzhingarov

The second monitor detail is the whole story. Every analytics tool I use has the same blind spot: a tab open on a side screen keeps logging time on page, so a page nobody has glanced at all afternoon looks like the best performing thing on the site. Same boolean pressed into meaning attention when it only ever meant visible.

Deleting your own clever code is the right ending too.

Collapse
 
dj29 profile image
Dhruv Jani

Exactly! That second-monitor scenario was the one I completely missed. document.hidden felt like the right signal until I realized it was measuring visibility, not attention. Thanks for sharing the analytics perspective too!

Collapse
 
lunsy profile image
Hopeful Apprentice

Great story! I've fallen into the same trap with document.hidden – it's so tempting to "optimize" UX, but the assumption that visible tab = attentive user is dangerous. Your point about multi‑monitor setups and multiple devices is spot on. Sometimes the simplest fix is just to delete the clever code. Thanks for sharing this lesson – it's a great reminder that reliability often beats micro‑optimizations. 👏

Collapse
 
dj29 profile image
Dhruv Jani

Exactly!! document.hidden felt like such a clever little optimization until I actually thought about all the ways “visible” ≠ “user is looking”.

And yeah, deleting the clever code was probably my favorite part of the whole bug 😂 Thanks for sharing your experience too, really glad it resonated!

Collapse
 
figsh profile image
FSCSS

Helpful article ❤️

Collapse
 
tanay_dwivedi9098 profile image
Tanay Dwivedi

Nice blog explaining the bug and how you solved it with a good storytelling @dj29

Collapse
 
dj29 profile image
Dhruv Jani

Thanks bro. Actually that's the thing We do a lot of bug fixes, projects in day-to-day life but this is the platform where can talk about it and find people who will vibe with it😅

Collapse
 
syedahmershah profile image
Syed Ahmer Shah

This is a great example of how a seemingly reasonable optimization can encode the wrong assumption. document.hidden tells you visibility, not attention, and the multi-monitor case makes that distinction painfully clear. The delivery-vs-seen separation is a much more robust mental model. Great debugging story! 👏

Collapse
 
dj29 profile image
Dhruv Jani

Exactly! document.hidden looked like the perfect signal until I realized I was using it to answer a question it was never meant to answer. 😅

The delivery-vs-seen distinction is definitely something I'm taking forward from this bug.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.