diff --git a/package.yaml b/package.yaml
index d656997f871e419cf9965b63f939614f053fb816..f261c960de2cb9483b1fee6c3cc92cec63a92f69 100644
--- a/package.yaml
+++ b/package.yaml
@@ -40,6 +40,7 @@ dependencies:
 - pipes-parse
 - pipes-safe
 - random
+- sorted-list
 - stm
 - text
 - time
diff --git a/src/QBar/Cli.hs b/src/QBar/Cli.hs
index 63840065d720cda90165185845ff37621e1a9433..958fd951392200cd6b4f90556fa8b6da5ff1d2a3 100644
--- a/src/QBar/Cli.hs
+++ b/src/QBar/Cli.hs
@@ -69,7 +69,8 @@ barConfigurationParser = do
 blockParser :: Parser (BarIO ())
 blockParser = subparser (
     commandGroup "Available blocks:" <>
-    command "date" (info (pure $ addBlock dateBlock) (progDesc "Load the date and time block."))
+    command "date" (info (pure $ addBlock dateBlock) (progDesc "Load the date and time block.")) <>
+    command "cpu" (info (pure $ addBlock $ cpuUsageBlock 1) (progDesc "Load the cpu usage block."))
   )
   <|>
   subparser (
diff --git a/src/QBar/Core.hs b/src/QBar/Core.hs
index 276299afe7f9588dd2d876e8f80c265890b130bd..d318a7fb0d2280efbfd256bb32362b46be543f56 100644
--- a/src/QBar/Core.hs
+++ b/src/QBar/Core.hs
@@ -5,8 +5,8 @@ module QBar.Core where
 
 import QBar.BlockOutput
 import QBar.TagParser
+import QBar.Time
 
-import Control.Concurrent (threadDelay)
 import Control.Concurrent.Async
 import Control.Concurrent.Event as Event
 import Control.Concurrent.MVar
@@ -18,8 +18,8 @@ import Control.Monad.State (StateT)
 import Control.Monad.Writer (WriterT)
 import Data.Aeson.TH
 import qualified Data.ByteString.Lazy.Char8 as C8
+import Data.Either (isRight)
 import Data.Int (Int64)
-import Data.Maybe (catMaybes)
 import qualified Data.Text.Lazy as T
 import qualified Data.Text.Lazy.Encoding as E
 import qualified Data.Text.Lazy.IO as TIO
@@ -71,6 +71,8 @@ class IsCachable a where
 
 instance IsCachable PushBlock where
   toCachedBlock = cachePushBlock
+instance IsCachable PullBlock where
+  toCachedBlock = toCachedBlock . schedulePullBlock
 instance IsCachable BlockCache where
   toCachedBlock = id
 
@@ -89,8 +91,13 @@ type BarIO = SafeT (ReaderT Bar IO)
 
 data Bar = Bar {
   requestBarUpdate :: IO (),
-  newBlockChan :: TChan BlockCache
+  newBlockChan :: TChan BlockCache,
+  barSleepScheduler :: SleepScheduler
 }
+instance HasSleepScheduler BarIO where
+  askSleepScheduler = barSleepScheduler <$> askBar
+instance HasSleepScheduler (Proxy a' a b' b BarIO) where
+  askSleepScheduler = lift askSleepScheduler
 
 
 newtype BarUpdateChannel = BarUpdateChannel (IO ())
@@ -142,11 +149,50 @@ updateEventHandler :: BlockEventHandler -> BlockState -> BlockState
 updateEventHandler _ Nothing = Nothing
 updateEventHandler eventHandler (Just (blockOutput, _)) = Just (blockOutput, Just eventHandler)
 
+hasEventHandler :: BlockState -> Bool
+hasEventHandler (Just (_, Just _)) = True
+hasEventHandler _ = False
+
+invalidateBlockState :: BlockState -> BlockState
+invalidateBlockState Nothing = Nothing
+invalidateBlockState (Just (output, eventHandler)) = Just (invalidateBlock output, eventHandler)
+
 
 runBarIO :: Bar -> BarIO r -> IO r
 runBarIO bar action = runReaderT (runSafeT action) bar
 
 
+defaultInterval :: Interval
+defaultInterval = everyNSeconds 10
+
+schedulePullBlock :: PullBlock -> PushBlock
+schedulePullBlock pullBlock = PushMode <$ pullBlock >-> sleepToNextInterval
+  where
+    sleepToNextInterval :: Pipe BlockState BlockState BarIO PullMode
+    sleepToNextInterval = do
+      event <- liftIO Event.new
+      forever $ do
+        state <- await
+        if hasEventHandler state
+          then do
+            -- If state already has an event handler, we do not attach another one
+            yield state
+            sleepUntilInterval defaultInterval
+          else do
+            -- Attach a click handler that will trigger a block update
+            yield $ updateEventHandler (triggerOnClick event) state
+
+            scheduler <- askSleepScheduler
+            result <- liftIO $ do
+              timerTask <- async $ sleepUntilInterval' scheduler defaultInterval
+              eventTask <- async $ Event.wait event
+              waitEitherCancel timerTask eventTask
+
+            when (isRight result) $ yield $ invalidateBlockState state
+
+    triggerOnClick :: Event -> BlockEvent -> BarIO ()
+    triggerOnClick event _ = liftIO $ Event.signal event
+
 newCache :: Producer [BlockState] IO () -> BlockCache
 newCache input = newCacheInternal =<< newCache''
   where
@@ -228,81 +274,6 @@ autoPadding = autoPadding' 0 0
     padShortText len = over (shortText._Just) $ \s -> padString (len - printedLength s) <> s
 
 
--- | Create a shared interval. Takes a BarUpdateChannel to signal bar updates and an interval (in seconds).Data.Maybe
--- Returns an IO action that can be used to attach blocks to the shared interval and an async that contains a reference to the scheduler thread.
-sharedInterval :: Int -> BarIO (PullBlock -> BlockCache)
-sharedInterval seconds = do
-  clientsMVar <- liftIO $ newMVar ([] :: [(MVar PullBlock, Output BlockState)])
-
-  startEvent <- liftIO Event.new
-
-  task <- barAsync $ do
-    -- Wait for at least one subscribed client
-    liftIO $ Event.wait startEvent
-    forever $ do
-      liftIO $ threadDelay $ seconds * 1000000
-      -- Updates all client blocks
-      -- If send returns 'False' the clients mailbox has been closed, so it is removed
-      bar <- askBar
-      liftIO $ modifyMVar_ clientsMVar $ fmap catMaybes . mapConcurrently (runBarIO bar . runAndFilterClient)
-      -- Then update the bar
-      updateBar
-
-  liftIO $ link task
-
-  return (addClient startEvent clientsMVar)
-  where
-    runAndFilterClient :: (MVar PullBlock, Output BlockState) -> BarIO (Maybe (MVar PullBlock, Output BlockState))
-    runAndFilterClient client = do
-      result <- runClient client
-      return $ if result then Just client else Nothing
-    runClient :: (MVar PullBlock, Output BlockState) -> BarIO Bool
-    runClient (blockMVar, output) = do
-      bar <- askBar
-      liftIO $ modifyMVar blockMVar $ \blockProducer -> do
-        result <- runReaderT (runSafeT $ next blockProducer) bar
-        case result of
-          Left _ -> return (exitBlock, False)
-          Right (blockState, blockProducer') -> do
-            success <- atomically $ send output $ updateEventHandler (updateClickHandler blockState) blockState
-            if success
-              -- Store new BlockProducer back into MVar
-              then return (blockProducer', True)
-              -- Mailbox is sealed, stop running producer
-              else return (exitBlock, False)
-      where
-        updateClickHandler :: BlockState -> BlockEvent -> BarIO ()
-        updateClickHandler Nothing _ = return ()
-        updateClickHandler (Just (block, _)) _ = do
-          -- Give user feedback that the block is updating
-          let outdatedBlock = invalidateBlock block
-          -- The invalidated block output has no event handler
-          liftIO $ void $ atomically $ send output . Just $ (outdatedBlock, Nothing)
-          -- Notify bar about changed block state to display the feedback
-          updateBar
-          -- Run a normal block update to update the block to the new value
-          void $ runClient (blockMVar, output)
-          -- Notify bar about changed block state, this is usually done by the shared interval handler
-          updateBar
-    addClient :: Event.Event -> MVar [(MVar PullBlock, Output BlockState)] -> PullBlock -> BlockCache
-    addClient startEvent clientsMVar blockProducer = do
-      -- Spawn the mailbox that preserves the latest block
-      (output, input) <- liftIO $ spawn $ latest Nothing
-
-      blockMVar <- liftIO $ newMVar blockProducer
-
-      -- Generate initial block and send it to the mailbox
-      lift $ void $ runClient (blockMVar, output)
-
-      -- Register the client for regular updates
-      liftIO $ modifyMVar_ clientsMVar $ \ clients -> return ((blockMVar, output):clients)
-
-      -- Start update thread (if not already started)
-      liftIO $ Event.set startEvent
-
-      -- Return a block producer from the mailbox
-      cacheFromInput input
-
 blockScript :: FilePath -> PullBlock
 blockScript path = forever $ updateBlock =<< (lift blockScriptAction)
   where
diff --git a/src/QBar/DefaultConfig.hs b/src/QBar/DefaultConfig.hs
index 7fc58cd76e6292628aaff71f7812decb8f83605f..ac5ad0849ab304fbefe491505ccfe4b2b8a34b97 100644
--- a/src/QBar/DefaultConfig.hs
+++ b/src/QBar/DefaultConfig.hs
@@ -10,10 +10,8 @@ import Control.Lens
 
 defaultBarConfig :: BarIO ()
 defaultBarConfig = do
-  systemInfoInterval <- sharedInterval 10
-
-  let battery = systemInfoInterval $ batteryBlock >-> modify (blockName ?~ "battery")
-  let cpuUsage = systemInfoInterval $ cpuUsageBlock 1 >-> modify ((blockName ?~ "cpuUsage") . addIcon "💻\xFE0E")
+  let battery = batteryBlock >-> modify (blockName ?~ "battery")
+  let cpuUsage = cpuUsageBlock 1 >-> modify ((blockName ?~ "cpuUsage") . addIcon "💻\xFE0E")
 
   -- TODO: commented-out blocks should be added as soon as they are implemented in qbar
   addBlock dateBlock
@@ -26,16 +24,14 @@ defaultBarConfig = do
 
 legacyBarConfig :: BarIO ()
 legacyBarConfig = do
-  systemInfoInterval <- sharedInterval 10
-
-  let todo = systemInfoInterval (blockScript $ blockLocation "todo")
-  let wifi = systemInfoInterval $ (blockScript $ blockLocation "wifi2") >-> modify (addIcon "📡\xFE0E")
-  let networkEnvironment = systemInfoInterval (blockScript $ blockLocation "network-environment")
-  let ram = systemInfoInterval $ (blockScript $ blockLocation "memory") >-> modify (addIcon "🐏\xFE0E") >-> autoPadding
-  let temperature = systemInfoInterval $ (blockScript $ blockLocation "temperature") >-> autoPadding
+  let todo = blockScript $ blockLocation "todo"
+  let wifi = (blockScript $ blockLocation "wifi2") >-> modify (addIcon "📡\xFE0E")
+  let networkEnvironment = blockScript $ blockLocation "network-environment"
+  let ram = (blockScript $ blockLocation "memory") >-> modify (addIcon "🐏\xFE0E") >-> autoPadding
+  let temperature = (blockScript $ blockLocation "temperature") >-> autoPadding
   let volumeBlock = persistentBlockScript $ blockLocation "volume-pulseaudio -S -F3"
-  let battery = systemInfoInterval $ batteryBlock >-> modify (blockName ?~ "battery")
-  let cpuUsage = systemInfoInterval $ cpuUsageBlock 1 >-> modify ((blockName ?~ "cpuUsage") . addIcon "💻\xFE0E")
+  let battery = batteryBlock >-> modify (blockName ?~ "battery")
+  let cpuUsage = cpuUsageBlock 1 >-> modify ((blockName ?~ "cpuUsage") . addIcon "💻\xFE0E")
 
   addBlock dateBlock
   addBlock battery
diff --git a/src/QBar/Host.hs b/src/QBar/Host.hs
index e191194a2eacd1463c156bb4e2d1add4bb930032..5934ceccf6c8b849cb7f7fb832e44a62862e3c4f 100644
--- a/src/QBar/Host.hs
+++ b/src/QBar/Host.hs
@@ -5,6 +5,7 @@ module QBar.Host where
 
 import QBar.BlockOutput
 import QBar.Core
+import QBar.Time
 
 import Control.Concurrent (forkIO, forkFinally, threadDelay)
 import Control.Concurrent.Event as Event
@@ -134,7 +135,9 @@ runBarHost createHost loadBlocks = do
   -- Create channel to send new block producers to render loop
   newBlockChan <- newTChanIO
 
-  let bar = Bar { requestBarUpdate, newBlockChan }
+  barSleepScheduler <- createSleepScheduler
+
+  let bar = Bar { requestBarUpdate, newBlockChan, barSleepScheduler }
 
   -- Install signal handler for SIGCONT
   installSignalHandlers bar
diff --git a/src/QBar/Time.hs b/src/QBar/Time.hs
index fa205f22f91d0e2ebb76ec51ecbd40e515417eae..9add2437994cbdfeb397851e887e162b16ab02c9 100644
--- a/src/QBar/Time.hs
+++ b/src/QBar/Time.hs
@@ -1,23 +1,136 @@
-module QBar.Time (sleepUntil, nextMinute) where
+{-# LANGUAGE OverloadedLists #-}
 
-import Control.Concurrent (threadDelay)
-import Data.Time.Clock (UTCTime, getCurrentTime, diffUTCTime, utctDayTime)
+module QBar.Time (SleepScheduler, HasSleepScheduler(..), Interval, createSleepScheduler, sleepUntil, sleepUntil', sleepUntilInterval, sleepUntilInterval', everyMinute, everyNSeconds, nextIntervalTime) where
 
-sleepUntil :: UTCTime -> IO ()
-sleepUntil time = do
-  now <- getCurrentTime
-  let timeUntil = diffUTCTime time now
-  when (timeUntil > 0) $
-    if timeUntil > 1
-      then threadDelay 1000000 >> sleepUntil time
-      else threadDelay $ ceiling $ toRational timeUntil * 1000000
-
-nextMinute :: IO UTCTime
-nextMinute = do
+import Control.Concurrent.Async
+import Control.Concurrent.MVar
+import qualified Control.Concurrent.Event as Event
+import Data.Time.Clock (UTCTime, getCurrentTime, diffUTCTime, utctDayTime, addUTCTime)
+import Data.SortedList (SortedList, toSortedList, fromSortedList, singleton, partition, insert)
+import Data.Ord (comparing)
+
+newtype Interval = IntervalSeconds Integer
+
+-- |Describes an interval that is run every "n" seconds after midnight.
+everyNSeconds :: Integer -> Interval
+everyNSeconds = IntervalSeconds
+
+-- |Describes an interval that is run every minute.
+everyMinute :: Interval
+everyMinute = IntervalSeconds 60
+
+nextIntervalTime :: MonadIO m => Interval -> m UTCTime
+nextIntervalTime (IntervalSeconds intervalSeconds) = liftIO $ do
   now <- getCurrentTime
   let dayTime = utctDayTime now
   let daySeconds = floor dayTime
-  let dayMinute = daySeconds `div` 60
+  let intervalId = daySeconds `div` intervalSeconds
   return now {
-    utctDayTime = fromInteger $ (dayMinute + 1) * 60
-  }
\ No newline at end of file
+    utctDayTime = fromInteger $ (intervalId + 1) * intervalSeconds
+  }
+
+
+data SleepScheduler = SleepScheduler (MVar (SortedList ScheduledEvent, [ScheduledEvent])) Event.Event
+data ScheduledEvent = ScheduledEvent {
+  time :: UTCTime,
+  event :: Event.Event,
+  fireOnNegativeTimeJump :: Bool
+} deriving Eq
+instance Ord ScheduledEvent where
+  compare = comparing time
+
+class HasSleepScheduler m where
+  askSleepScheduler :: m SleepScheduler
+
+createSleepScheduler :: MonadIO m => m SleepScheduler
+createSleepScheduler = liftIO $ do
+  scheduler <- SleepScheduler <$> newMVar ([], []) <*> Event.new
+  link =<< (async $ schedulerThread scheduler)
+  return scheduler
+  where
+    schedulerThread :: SleepScheduler -> IO ()
+    schedulerThread (SleepScheduler eventsMVar trigger) = schedulerThread' =<< getCurrentTime
+      where
+        schedulerThread' :: UTCTime -> IO ()
+        schedulerThread' lastTime = do
+          start <- getCurrentTime
+
+          -- Check for a negative time step (threshold is between 5 and 65 seconds, depending on loop activity)
+          when (start < addUTCTime (fromInteger (-5)) lastTime) $ fireEvents fireOnNegativeTimeJump
+
+          (sortedEvents, _) <- readMVar eventsMVar
+          waitResult <- case fromSortedList sortedEvents of
+            [] -> True <$ Event.wait trigger
+            (ScheduledEvent{time} : _) -> waitForEvent time
+
+          when waitResult $ do
+            now <- getCurrentTime
+            fireEvents (checkEvent now)
+
+          schedulerThread' start
+
+        -- |Waits for the next event, with a timeout. A return value of 'False' indicates a timeout occured.
+        waitForEvent :: UTCTime -> IO Bool
+        waitForEvent eventTime = do
+          now <- getCurrentTime
+          let timeUntil = diffUTCTime eventTime now
+          if
+            | timeUntil <= 0 -> return True
+            | timeUntil < 60 -> True <$ Event.waitTimeout trigger (ceiling $ toRational timeUntil * 1000000)
+            -- False indicates a timeout, in which case no events need to be fired
+            | otherwise -> Event.waitTimeout trigger (60 * 1000000)
+
+
+        fireEvents :: (ScheduledEvent -> Bool) -> IO ()
+        fireEvents predicate =
+          modifyMVar_ eventsMVar $ \(hots, colds) -> do
+            let allEvents = hots <> toSortedList colds
+            let (activeEvents, futureEvents) = partition predicate allEvents
+            mapM_ (Event.set . event) activeEvents
+            -- Sleep scheduler thread 'Event' is cleared during 'modifyMVar_' to prevent race conditions.
+            Event.clear trigger
+            return (futureEvents, [])
+
+        -- |Predicate to check if an event should be fired.
+        checkEvent :: UTCTime -> ScheduledEvent -> Bool
+        checkEvent now ScheduledEvent{time} = now >= time
+
+
+queueScheduledEvent :: MonadIO m => SleepScheduler -> ScheduledEvent -> m ()
+queueScheduledEvent (SleepScheduler eventsMVar trigger) event@ScheduledEvent{time=eventTime} = liftIO $
+  modifyMVar_ eventsMVar $ \(sorted, unsorted) ->
+    -- Sleep scheduler thread 'Event' is set during 'modifyMVar_' to prevent race conditions.
+    case fromSortedList sorted of
+      [] -> (singleton event, unsorted) <$ Event.set trigger
+      (first : _) ->
+        if eventTime < time first
+          -- Event happens before the first event, so it is inserted at the front of the sorted list and the scheduler thread is notified
+          then (insert event sorted, unsorted) <$ Event.set trigger
+          -- Otherwise it is added to the unsorted pool and will be handled later.
+          else return (sorted, event:unsorted)
+
+
+-- |Suspends the thread until the given time is reached.
+sleepUntil :: (HasSleepScheduler m, MonadIO m) => UTCTime -> m ()
+sleepUntil time = do
+  scheduler <- askSleepScheduler
+  sleepUntil' scheduler time
+
+sleepUntil' :: MonadIO m => SleepScheduler -> UTCTime -> m ()
+sleepUntil' scheduler time = liftIO $ do
+  event <- Event.new
+  queueScheduledEvent scheduler (ScheduledEvent {time, event, fireOnNegativeTimeJump=False})
+  Event.wait event
+
+-- |Suspends the thread until the next time boundary described by 'Interval' is reached. Also returns when the system time jumps backwards.
+sleepUntilInterval :: (HasSleepScheduler m, MonadIO m) => Interval -> m ()
+sleepUntilInterval interval = do
+  scheduler <- askSleepScheduler
+  sleepUntilInterval' scheduler interval
+
+sleepUntilInterval' :: MonadIO m => SleepScheduler -> Interval -> m ()
+sleepUntilInterval' scheduler interval = liftIO $ do
+  event <- Event.new
+  time <- nextIntervalTime interval
+  queueScheduledEvent scheduler (ScheduledEvent {time, event, fireOnNegativeTimeJump=True})
+  Event.wait event