diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,11 @@
 
 ## Unreleased
 
+## 0.3.2.13 - 2026-03-30
+- Host: suppress no-op property update notifications when a signal arrives but
+  the refreshed item value is unchanged, avoiding redundant tray updates.
+- Add a regression test covering unchanged-vs-changed `NewTitle` updates.
+
 ## 0.3.2.12 - 2026-03-30
 - Fix the `HostSpec` test helper's overloaded-string inference so the test
   suite builds cleanly with newer GHC/`dbus` combinations, restoring the
diff --git a/src/StatusNotifier/Host/Service.hs b/src/StatusNotifier/Host/Service.hs
--- a/src/StatusNotifier/Host/Service.hs
+++ b/src/StatusNotifier/Host/Service.hs
@@ -532,9 +532,15 @@
         updateItemByLensAndProp lens prop busName = runExceptT $ do
           newValue <- ExceptT (runProperty prop busName)
           let modify infoMap =
-                -- This noops when the value is not present
-                let newMap = set (at busName . _Just . lens) newValue infoMap
-                 in return (newMap, Map.lookup busName newMap)
+                case Map.lookup busName infoMap of
+                  Nothing -> return (infoMap, Nothing)
+                  Just oldInfo ->
+                    let newInfo = set lens newValue oldInfo
+                     in if newInfo == oldInfo
+                          then return (infoMap, Just Nothing)
+                          else
+                            let newMap = Map.insert busName newInfo infoMap
+                             in return (newMap, Just $ Just newInfo)
           ExceptT $
             maybeToEither (methodError (Serial 0) errorFailed)
               <$> modifyMVar itemInfoMapVar modify
@@ -546,9 +552,9 @@
         -- will succeed.
         runUpdatersForService updaters updateType serviceName = do
           updateResults <- mapM ($ serviceName) updaters
-          let (failures, updates) = partitionEithers updateResults
-              logLevel = propertyUpdateFailureLogLevel failures updates
-          mapM_ (doUpdate updateType) updates
+          let (failures, successes) = partitionEithers updateResults
+              logLevel = propertyUpdateFailureLogLevel failures successes
+          mapM_ (doUpdate updateType) $ catMaybes successes
           when (not $ null failures) $
             hostLogger logLevel $
               printf "Property update failures %s" $
diff --git a/status-notifier-item.cabal b/status-notifier-item.cabal
--- a/status-notifier-item.cabal
+++ b/status-notifier-item.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           status-notifier-item
-version:        0.3.2.12
+version:        0.3.2.13
 synopsis:       A wrapper over the StatusNotifierItem/libappindicator dbus specification
 description:    Please see the README on Github at <https://github.com/taffybar/status-notifier-item#readme>
 category:       Desktop
diff --git a/test/HostSpec.hs b/test/HostSpec.hs
--- a/test/HostSpec.hs
+++ b/test/HostSpec.hs
@@ -6,8 +6,11 @@
   ( forkIO,
     newChan,
     newEmptyMVar,
+    newMVar,
     putMVar,
     readChan,
+    readMVar,
+    swapMVar,
     takeMVar,
     threadDelay,
     tryPutMVar,
@@ -15,7 +18,7 @@
   )
 import Control.Exception (finally)
 import Control.Monad (replicateM, void)
-import DBus (busName_, objectPath_)
+import DBus (busName_, interfaceName_, memberName_, objectPath_, signal)
 import DBus.Client
 import qualified DBus.Internal.Message as M
 import DBus.Internal.Types (ErrorName, Serial (..), errorName_)
@@ -370,6 +373,55 @@
       first `shouldBe` Just (ItemAdded, busName_ itemName)
       second <- timeout 300000 (readChan events)
       second `shouldBe` Nothing
+
+      _ <- releaseName itemClient (busName_ itemName)
+      pure ()
+
+    it "suppresses property update signals when the item value is unchanged" $ \() -> do
+      _watcher <- startWatcher
+      hostClient <- connectSession
+      Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-k"}
+
+      let itemName = "org.test.HostNoopTitleUpdate"
+      titleVar <- newMVar ("Initial Title" :: String)
+      itemClient <- connectSession
+      let emitNewTitle =
+            emit itemClient $
+              signal
+                (objectPath_ defaultPath)
+                (interfaceName_ "org.kde.StatusNotifierItem")
+                (memberName_ "NewTitle")
+          iface =
+            Interface
+              { interfaceName = "org.kde.StatusNotifierItem",
+                interfaceMethods = [],
+                interfaceProperties =
+                  [ readOnlyProperty "Title" (readMVar titleVar),
+                    readOnlyProperty "IconName" (pure ("folder" :: String)),
+                    readOnlyProperty "OverlayIconName" (pure ("" :: String)),
+                    readOnlyProperty "ItemIsMenu" (pure False)
+                  ],
+                interfaceSignals = []
+              }
+      export itemClient (objectPath_ defaultPath) iface
+      _ <- requestName itemClient (busName_ itemName) []
+      WatcherClient.registerStatusNotifierItem itemClient itemName
+        `shouldReturn` Right ()
+
+      events <- newChan
+      _ <- addUpdateHandler host (\ut info -> writeChan events (ut, iconTitle info))
+
+      added <- timeout 1500000 (readChan events)
+      added `shouldBe` Just (ItemAdded, "Initial Title")
+
+      emitNewTitle
+      unchanged <- timeout 300000 (readChan events)
+      unchanged `shouldBe` Nothing
+
+      _ <- swapMVar titleVar "Updated Title"
+      emitNewTitle
+      updated <- timeout 1500000 (readChan events)
+      updated `shouldBe` Just (TitleUpdated, "Updated Title")
 
       _ <- releaseName itemClient (busName_ itemName)
       pure ()
