diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,19 @@
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [1.7.9] - 2025-06-02
+
+### Fixed
+
+- fix handling of optional `!include` nix.conf statements that would error if the file was missing
+- daemon: improve event loop responsiveness to shutdown signals
+- daemon: gracefully handle sigTERM and sigINT signals
+
+### Changed
+
+- increase event loop size to handle rapid pushing of many small paths
+- update dependencies
+
 ## [1.7.8] - 2025-04-09
 
 ### Fixed
diff --git a/cachix.cabal b/cachix.cabal
--- a/cachix.cabal
+++ b/cachix.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               cachix
-version:            1.7.8
+version:            1.7.9
 synopsis:
   Command-line client for Nix binary cache hosting https://cachix.org
 
diff --git a/src/Cachix/Client/NixConf.hs b/src/Cachix/Client/NixConf.hs
--- a/src/Cachix/Client/NixConf.hs
+++ b/src/Cachix/Client/NixConf.hs
@@ -39,6 +39,7 @@
 import Cachix.Client.Exception (CachixException (..))
 import Cachix.Client.URI qualified as URI
 import Cachix.Types.BinaryCache qualified as BinaryCache
+import Control.Exception.Safe qualified as Safe
 import Data.List (nub)
 import Data.Text qualified as T
 import Protolude hiding (toS)
@@ -46,11 +47,12 @@
 import System.Directory
   ( XdgDirectory (..),
     createDirectoryIfMissing,
-    doesFileExist,
     getXdgDirectory,
   )
 import System.FilePath (normalise)
 import System.FilePath.Posix (takeDirectory, (</>))
+import System.IO.Error (isDoesNotExistError)
+import System.IO.Error qualified
 import Text.Megaparsec qualified as Mega
 import Text.Megaparsec.Char
 
@@ -89,6 +91,13 @@
   }
   deriving stock (Show, Eq, Functor)
 
+data NixConfError
+  = -- | Error when trying to read the nix.conf file or an include
+    IOError FilePath System.IO.Error.IOError
+  | -- | Failed to parse the nix.conf file
+    ParseError FilePath Text
+  deriving (Show, Typeable)
+
 -- | Operations on nix.conf.
 -- Helps to work with both NixConf and NixConfSource.
 class NixConfOps a where
@@ -199,17 +208,22 @@
 
       if fullPath `elem` stack
         then case includeType of
-          RequiredInclude _ -> throwIO $ CircularInclude (formatCircularError fullPath)
+          RequiredInclude _ ->
+            throwIO $ CircularInclude (formatCircularError fullPath)
           OptionalInclude _ -> return []
         else do
-          exists <- doesFileExist fullPath
-          if not exists && isRequired includeType
-            then throwIO $ IncludeNotFound $ toS fullPath
-            else do
-              content <- readFile fullPath
-              case parse content of
-                Left _err -> return []
-                Right conf -> resolveIncludesWithStack (fullPath : stack) baseFile (NixConfSource fullPath conf)
+          read' fullPath >>= \case
+            Left err@(IOError _ _) ->
+              if isRequired includeType
+                then do
+                  printNixConfError err
+                  throwIO $ IncludeNotFound ("Failed to read required include file: " <> toS fullPath)
+                else return []
+            Left err@(ParseError _ _) -> do
+              printNixConfError err
+              throwIO $ IncludeNotFound ("Failed to read required include file: " <> toS fullPath)
+            Right conf ->
+              resolveIncludesWithStack (fullPath : stack) baseFile conf
 
     isRequired (RequiredInclude _) = True
     isRequired (OptionalInclude _) = False
@@ -231,29 +245,40 @@
 data NixConfLoc = Global | Local | Custom FilePath
   deriving stock (Show, Eq)
 
+-- | Safely read a nix.conf file from the given location.
+-- Prints errors to stderr.
 read :: NixConfLoc -> IO (Maybe NixConfSource)
 read ncl = do
   filename <- getFilename ncl
-  read' filename
-
-read' :: FilePath -> IO (Maybe NixConfSource)
-read' filename = do
-  doesExist <- doesFileExist filename
-  if not doesExist
-    then return Nothing
-    else do
-      result <- parse <$> readFile filename
-      case result of
-        Left err -> do
-          putStrLn (Mega.errorBundlePretty err)
-          panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"
-        Right conf -> return (Just (NixConfSource filename conf))
+  read' filename >>= \case
+    Left err -> do
+      printNixConfError err
+      return Nothing
+    Right conf -> return $ Just conf
 
+-- | Safely read a nix.conf file from the given location.
+-- Return an empty NixConfSource if the file does not exist or cannot be read.
+-- Prints errors to stderr.
 readWithDefault :: NixConfLoc -> IO NixConfSource
 readWithDefault ncl = do
   filename <- getFilename ncl
-  fromMaybe (new filename) <$> read' filename
+  read' filename >>= \case
+    Left err -> do
+      printNixConfError err
+      return $ new filename
+    Right conf -> return conf
 
+-- | Safely read a nix.conf file from the given file path.
+read' :: FilePath -> IO (Either NixConfError NixConfSource)
+read' filename = do
+  econtent <- Safe.tryIO (readFile filename)
+  return $ case econtent of
+    Left err -> Left $ IOError filename err
+    Right content ->
+      case parse content of
+        Left err -> Left $ ParseError filename $ toS (Mega.errorBundlePretty err)
+        Right conf -> Right $ NixConfSource filename conf
+
 getFilename :: NixConfLoc -> IO FilePath
 getFilename ncl = do
   dir <-
@@ -262,6 +287,29 @@
       Local -> getXdgDirectory XdgConfig "nix"
       Custom filepath -> return filepath
   return $ dir <> "/nix.conf"
+
+printNixConfError :: NixConfError -> IO ()
+printNixConfError (IOError path err) | isDoesNotExistError err = do
+  putErrText $
+    unlines
+      [ "No config at " <> toS path <> ":",
+        "",
+        toS (displayException err)
+      ]
+printNixConfError (IOError path err) = do
+  putErrText $
+    unlines
+      [ "Failed to read " <> toS path <> ":",
+        "",
+        toS (displayException err)
+      ]
+printNixConfError (ParseError path err) = do
+  putErrText $
+    unlines
+      [ "Failed to parse " <> toS path <> ":",
+        "",
+        err
+      ]
 
 -- nix.conf Parser
 type Parser = Mega.Parsec Void Text
diff --git a/src/Cachix/Daemon.hs b/src/Cachix/Daemon.hs
--- a/src/Cachix/Daemon.hs
+++ b/src/Cachix/Daemon.hs
@@ -23,7 +23,6 @@
 import Cachix.Daemon.Protocol as Protocol
 import Cachix.Daemon.Push as Push
 import Cachix.Daemon.PushManager qualified as PushManager
-import Cachix.Daemon.ShutdownLatch
 import Cachix.Daemon.SocketStore qualified as SocketStore
 import Cachix.Daemon.Subscription as Subscription
 import Cachix.Daemon.Types as Types
@@ -33,6 +32,7 @@
 import Cachix.Types.BinaryCache qualified as BinaryCache
 import Control.Concurrent.STM.TMChan
 import Control.Exception.Safe (catchAny)
+import Data.IORef (IORef, atomicModifyIORef', newIORef)
 import Data.Text qualified as T
 import Hercules.CNix.Store (Store, withStore)
 import Hercules.CNix.Util qualified as CNix.Util
@@ -43,7 +43,7 @@
 import System.IO.Error (isResourceVanishedError)
 import System.Posix.Process (getProcessID)
 import System.Posix.Signals qualified as Signal
-import UnliftIO (MonadUnliftIO)
+import UnliftIO (MonadUnliftIO, withRunInIO)
 import UnliftIO.Async qualified as Async
 import UnliftIO.Exception (bracket)
 
@@ -71,7 +71,6 @@
   daemonLogger <- Log.new "cachix.daemon" daemonLogHandle daemonLogLevel
 
   daemonEventLoop <- EventLoop.new
-  daemonShutdownLatch <- newShutdownLatch
   daemonPid <- getProcessID
 
   daemonSocketPath <- maybe getSocketPath pure (Options.daemonSocketPath daemonOptions)
@@ -99,7 +98,7 @@
 start daemonEnv daemonOptions daemonPushOptions daemonCacheName =
   withStore $ \store -> do
     daemon <- new daemonEnv store daemonOptions Nothing daemonPushOptions daemonCacheName
-    installSignalHandlers daemon
+    void $ runDaemon daemon installSignalHandlers
     result <- run daemon
     exitWith (toExitCode result)
 
@@ -156,15 +155,53 @@
 stopIO DaemonEnv {daemonEventLoop} =
   EventLoop.sendIO daemonEventLoop ShutdownGracefully
 
-installSignalHandlers :: DaemonEnv -> IO ()
-installSignalHandlers daemon = do
-  for_ [Signal.sigTERM, Signal.sigINT] $ \signal ->
-    Signal.installHandler signal (Signal.CatchOnce handler) Nothing
+installSignalHandlers :: Daemon ()
+installSignalHandlers = do
+  withRunInIO $ \runInIO -> do
+    mainThreadId <- myThreadId
+    -- Track Ctrl+C attempts
+    interruptRef <- newIORef False
+
+    -- Install signal handlers using runInIO to properly run Daemon actions from IO
+    _ <- Signal.installHandler Signal.sigTERM (Signal.Catch (runInIO (termHandler mainThreadId))) Nothing
+    _ <- Signal.installHandler Signal.sigINT (Signal.Catch (runInIO (intHandler mainThreadId interruptRef))) Nothing
+
+    return ()
   where
-    handler = do
-      CNix.Util.triggerInterrupt
-      stopIO daemon
+    -- SIGTERM: Trigger immediate shutdown
+    termHandler :: ThreadId -> Daemon ()
+    termHandler mainThreadId = do
+      Katip.logFM Katip.InfoS "sigTERM received. Exiting immediately..."
+      startExitTimer mainThreadId
+      liftIO CNix.Util.triggerInterrupt
+      eventLoop <- asks daemonEventLoop
+      -- Signal directly to the event loop to ensure exit even if queue is full
+      EventLoop.exitLoopWithFailure EventLoopClosed eventLoop
 
+    -- SIGINT: First try to shutdown gracefully, on second press force exit
+    intHandler :: ThreadId -> IORef Bool -> Daemon ()
+    intHandler mainThreadId interruptRef = do
+      liftIO CNix.Util.triggerInterrupt
+      isSecondInterrupt <- liftIO $ atomicModifyIORef' interruptRef (True,)
+      eventLoop <- asks daemonEventLoop
+
+      if isSecondInterrupt
+        then do
+          Katip.logFM Katip.InfoS "Exiting immediately..."
+          startExitTimer mainThreadId
+          -- Force shutdown at the event loop level to ensure exit even if queue is full
+          EventLoop.exitLoopWithFailure EventLoopClosed eventLoop
+        else do
+          Katip.logFM Katip.InfoS "Shutting down gracefully (Ctrl+C again to force exit)..."
+          EventLoop.send eventLoop ShutdownGracefully
+
+    -- Start a timer to ensure we exit even if the event loop hangs
+    startExitTimer :: ThreadId -> Daemon ()
+    startExitTimer mainThreadId = do
+      void $ liftIO $ forkIO $ do
+        threadDelay (15 * 1000 * 1000) -- 15 seconds
+        throwTo mainThreadId ExitSuccess
+
 queueJob :: Protocol.PushRequest -> Daemon ()
 queueJob pushRequest = do
   daemonPushManager <- asks daemonPushManager
@@ -207,9 +244,6 @@
 shutdownGracefully :: Daemon (Either DaemonError ())
 shutdownGracefully = do
   DaemonEnv {..} <- ask
-
-  -- Indicate that the daemon is shutting down
-  initiateShutdown daemonShutdownLatch
 
   -- Stop the push manager and wait for any remaining paths to be uploaded
   shutdownPushManager daemonPushManager
diff --git a/src/Cachix/Daemon/EventLoop.hs b/src/Cachix/Daemon/EventLoop.hs
--- a/src/Cachix/Daemon/EventLoop.hs
+++ b/src/Cachix/Daemon/EventLoop.hs
@@ -4,11 +4,14 @@
     sendIO,
     run,
     exitLoopWith,
+    exitLoopWithFailure,
     EventLoop,
   )
 where
 
+import Cachix.Daemon.ShutdownLatch qualified as ShutdownLatch
 import Cachix.Daemon.Types.EventLoop (EventLoop (..), EventLoopError (..))
+import Control.Concurrent.STM
 import Control.Concurrent.STM.TBMQueue
   ( isFullTBMQueue,
     newTBMQueueIO,
@@ -21,9 +24,9 @@
 
 new :: (MonadIO m) => m (EventLoop event a)
 new = do
-  exitLatch <- liftIO newEmptyMVar
-  queue <- liftIO $ newTBMQueueIO 100
-  return $ EventLoop {queue, exitLatch}
+  shutdownLatch <- ShutdownLatch.newShutdownLatch
+  queue <- liftIO $ newTBMQueueIO 100_000
+  return $ EventLoop {queue, shutdownLatch}
 
 -- | Send an event to the event loop with logging.
 send :: (Katip.KatipContext m) => EventLoop event a -> event -> m ()
@@ -38,41 +41,67 @@
     logger _ _ = return ()
 
 send' :: (MonadIO m) => (Katip.Severity -> Katip.LogStr -> m ()) -> EventLoop event a -> event -> m ()
-send' logger eventloop@(EventLoop {queue}) event = do
-  res <- liftIO $ atomically $ tryWriteTBMQueue queue event
-  case res of
-    -- The queue is closed.
-    Nothing ->
-      logger Katip.DebugS "Ignored an event because the event loop is closed"
-    -- Successfully wrote to the queue
-    Just True -> return ()
-    -- Failed to write to the queue
-    Just False -> do
-      isFull <- liftIO $ atomically $ isFullTBMQueue queue
-      let message =
-            if isFull
-              then "Event loop is full"
-              else "Unknown error"
-      logger Katip.ErrorS $ "Failed to write to event loop: " <> message
-      exitLoopWithFailure EventLoopFull eventloop
+send' logger eventloop@(EventLoop {queue, shutdownLatch}) event = do
+  -- First check if shutdown has been requested
+  isExiting <- ShutdownLatch.isShuttingDown shutdownLatch
+  if isExiting
+    then logger Katip.DebugS "Ignored an event because the event loop is shutting down"
+    else do
+      res <- liftIO $ atomically $ tryWriteTBMQueue queue event
+      case res of
+        -- The queue is closed.
+        Nothing ->
+          logger Katip.DebugS "Ignored an event because the event loop is closed"
+        -- Successfully wrote to the queue
+        Just True -> return ()
+        -- Failed to write to the queue
+        Just False -> do
+          isFull <- liftIO $ atomically $ isFullTBMQueue queue
+          let message =
+                if isFull
+                  then "Event loop is full"
+                  else "Unknown error"
+          logger Katip.ErrorS $ "Failed to write to event loop: " <> message
+          exitLoopWithFailure EventLoopFull eventloop
 
 -- | Run the event loop until it exits with 'exitLoopWith'.
 run :: (MonadIO m) => EventLoop event a -> (event -> m ()) -> m (Either EventLoopError a)
-run eventloop f = do
+run (EventLoop {queue, shutdownLatch}) f =
   fix $ \loop -> do
-    mevent <- liftIO $ atomically $ readTBMQueue (queue eventloop)
-    case mevent of
-      Just event -> f event
-      Nothing -> exitLoopWithFailure EventLoopClosed eventloop
+    -- Wait for either a shutdown signal or a message from the queue
+    eitherResult <-
+      liftIO $
+        atomically $
+          fmap Left (ShutdownLatch.waitForShutdownSTM shutdownLatch)
+            `orElse`
+            -- Try to read from queue
+            ( do
+                mevent <- readTBMQueue queue
+                case mevent of
+                  -- Got an event, return it
+                  Just event -> return $ Right event
+                  -- Queue is closed, signal shutdown
+                  Nothing -> do
+                    ShutdownLatch.initiateShutdownWithResultSTM (Left EventLoopClosed) shutdownLatch
+                    result <- ShutdownLatch.waitForShutdownSTM shutdownLatch
+                    return $ Left result
+            )
 
-    liftIO (tryReadMVar (exitLatch eventloop)) >>= \case
-      Just exitValue -> return exitValue
-      Nothing -> loop
+    -- Process the result
+    case eitherResult of
+      -- Shutdown requested, return the result
+      Left result -> return result
+      -- Got an event, process it and continue looping
+      Right event -> do
+        f event
+        loop
 
 -- | Short-circuit the event loop and exit with a given return value.
 exitLoopWith :: (MonadIO m) => a -> EventLoop event a -> m ()
-exitLoopWith exitValue (EventLoop {exitLatch}) = void $ liftIO $ tryPutMVar exitLatch (Right exitValue)
+exitLoopWith exitValue (EventLoop {shutdownLatch}) =
+  ShutdownLatch.initiateShutdown exitValue shutdownLatch
 
 -- | Short-circuit the event loop in case of an internal error.
 exitLoopWithFailure :: (MonadIO m) => EventLoopError -> EventLoop event a -> m ()
-exitLoopWithFailure err (EventLoop {exitLatch}) = void $ liftIO $ tryPutMVar exitLatch (Left err)
+exitLoopWithFailure err (EventLoop {shutdownLatch}) =
+  ShutdownLatch.initiateShutdownWithResult (Left err) shutdownLatch
diff --git a/src/Cachix/Daemon/ShutdownLatch.hs b/src/Cachix/Daemon/ShutdownLatch.hs
--- a/src/Cachix/Daemon/ShutdownLatch.hs
+++ b/src/Cachix/Daemon/ShutdownLatch.hs
@@ -3,24 +3,70 @@
     newShutdownLatch,
     waitForShutdown,
     initiateShutdown,
+    initiateShutdownWithResult,
+    getResult,
     isShuttingDown,
+    -- STM operations
+    isShuttingDownSTM,
+    initiateShutdownSTM,
+    initiateShutdownWithResultSTM,
+    getResultSTM,
+    waitForShutdownSTM,
   )
 where
 
-import Control.Concurrent.MVar
+import Control.Concurrent.STM
 import Protolude
 
 -- | A latch to keep track of the shutdown process.
-newtype ShutdownLatch = ShutdownLatch {unShutdownLatch :: MVar ()}
+-- A shutdown latch holds a result value (Either e a) when shutdown is initiated.
+-- Nothing means that shutdown has not been requested yet.
+newtype ShutdownLatch e a = ShutdownLatch {unShutdownLatch :: TVar (Maybe (Either e a))}
 
-newShutdownLatch :: (MonadIO m) => m ShutdownLatch
-newShutdownLatch = ShutdownLatch <$> liftIO newEmptyMVar
+-- | Create a new shutdown latch
+newShutdownLatch :: (MonadIO m) => m (ShutdownLatch e a)
+newShutdownLatch = ShutdownLatch <$> liftIO (newTVarIO Nothing)
 
-waitForShutdown :: (MonadIO m) => ShutdownLatch -> m ()
-waitForShutdown = liftIO . readMVar . unShutdownLatch
+-- | Block until shutdown is requested and return the result
+waitForShutdown :: (MonadIO m) => ShutdownLatch e a -> m (Either e a)
+waitForShutdown latch = liftIO $ atomically $ waitForShutdownSTM latch
 
-initiateShutdown :: (MonadIO m) => ShutdownLatch -> m ()
-initiateShutdown = void . liftIO . flip tryPutMVar () . unShutdownLatch
+-- | Signal shutdown with a "success" result
+initiateShutdown :: (MonadIO m) => a -> ShutdownLatch e a -> m ()
+initiateShutdown val latch = liftIO $ atomically $ initiateShutdownSTM val latch
 
-isShuttingDown :: (MonadIO m) => ShutdownLatch -> m Bool
-isShuttingDown = liftIO . fmap not . isEmptyMVar . unShutdownLatch
+-- | Signal shutdown with a specific result
+initiateShutdownWithResult :: (MonadIO m) => Either e a -> ShutdownLatch e a -> m ()
+initiateShutdownWithResult result latch = liftIO $ atomically $ initiateShutdownWithResultSTM result latch
+
+-- | Get the shutdown result if available
+getResult :: (MonadIO m) => ShutdownLatch e a -> m (Maybe (Either e a))
+getResult latch = liftIO $ atomically $ getResultSTM latch
+
+-- | Check if shutdown has been requested
+isShuttingDown :: (MonadIO m) => ShutdownLatch e a -> m Bool
+isShuttingDown latch = liftIO $ atomically $ isShuttingDownSTM latch
+
+-- STM Operations for use in atomic transactions
+
+-- | Check if shutdown is requested
+isShuttingDownSTM :: ShutdownLatch e a -> STM Bool
+isShuttingDownSTM latch = isJust <$> readTVar (unShutdownLatch latch)
+
+-- | Signal shutdown with a "success" result
+initiateShutdownSTM :: a -> ShutdownLatch e a -> STM ()
+initiateShutdownSTM val latch = writeTVar (unShutdownLatch latch) (Just (Right val))
+
+-- | Signal shutdown with a specific result
+initiateShutdownWithResultSTM :: Either e a -> ShutdownLatch e a -> STM ()
+initiateShutdownWithResultSTM result latch = writeTVar (unShutdownLatch latch) (Just result)
+
+-- | Get the shutdown result (if available)
+getResultSTM :: ShutdownLatch e a -> STM (Maybe (Either e a))
+getResultSTM = readTVar . unShutdownLatch
+
+-- | Block until shutdown is requested, then return the result
+waitForShutdownSTM :: ShutdownLatch e a -> STM (Either e a)
+waitForShutdownSTM latch = do
+  mresult <- readTVar (unShutdownLatch latch)
+  maybe retry return mresult
diff --git a/src/Cachix/Daemon/Types.hs b/src/Cachix/Daemon/Types.hs
--- a/src/Cachix/Daemon/Types.hs
+++ b/src/Cachix/Daemon/Types.hs
@@ -9,6 +9,9 @@
     DaemonError (..),
     HasExitCode (..),
 
+    -- * EventLoop errors
+    EventLoop.EventLoopError (..),
+
     -- * Log
     LogLevel (..),
 
@@ -27,6 +30,7 @@
 
 import Cachix.Daemon.Types.Daemon
 import Cachix.Daemon.Types.Error
+import Cachix.Daemon.Types.EventLoop as EventLoop
 import Cachix.Daemon.Types.Log
 import Cachix.Daemon.Types.PushEvent as PushEvent
 import Cachix.Daemon.Types.PushManager as PushManager
diff --git a/src/Cachix/Daemon/Types/Daemon.hs b/src/Cachix/Daemon/Types/Daemon.hs
--- a/src/Cachix/Daemon/Types/Daemon.hs
+++ b/src/Cachix/Daemon/Types/Daemon.hs
@@ -15,7 +15,6 @@
 import Cachix.Client.OptionsParser (PushOptions)
 import Cachix.Daemon.Log qualified as Log
 import Cachix.Daemon.Protocol qualified as Protocol
-import Cachix.Daemon.ShutdownLatch (ShutdownLatch)
 import Cachix.Daemon.Subscription (SubscriptionManager)
 import Cachix.Daemon.Types.Error (DaemonError (DaemonUnhandledException), UnhandledException (..))
 import Cachix.Daemon.Types.EventLoop (EventLoop)
@@ -73,8 +72,6 @@
     daemonSubscriptionManagerThread :: MVar (Async ()),
     -- | Logging env
     daemonLogger :: Logger,
-    -- | Shutdown latch
-    daemonShutdownLatch :: ShutdownLatch,
     -- | The PID of the daemon process
     daemonPid :: ProcessID
   }
diff --git a/src/Cachix/Daemon/Types/EventLoop.hs b/src/Cachix/Daemon/Types/EventLoop.hs
--- a/src/Cachix/Daemon/Types/EventLoop.hs
+++ b/src/Cachix/Daemon/Types/EventLoop.hs
@@ -1,22 +1,19 @@
 module Cachix.Daemon.Types.EventLoop
   ( EventLoop (..),
     EventLoopError (..),
-    ExitLatch,
   )
 where
 
+import Cachix.Daemon.ShutdownLatch (ShutdownLatch)
 import Control.Concurrent.STM.TBMQueue (TBMQueue)
 import Protolude
 
 -- | An event loop that processes a queue of events.
 data EventLoop event output = EventLoop
   { queue :: TBMQueue event,
-    exitLatch :: ExitLatch output
+    -- | Latch for signaling shutdown and returning results
+    shutdownLatch :: ShutdownLatch EventLoopError output
   }
-
--- | An exit latch is a semaphore that signals the event loop to exit.
--- The exit code should be returned by the 'EventLoop'.
-type ExitLatch a = MVar (Either EventLoopError a)
 
 data EventLoopError
   = EventLoopClosed
diff --git a/src/Cachix/Deploy/Agent.hs b/src/Cachix/Deploy/Agent.hs
--- a/src/Cachix/Deploy/Agent.hs
+++ b/src/Cachix/Deploy/Agent.hs
@@ -186,11 +186,38 @@
             tryToAcquireLock (attempts + 1)
 
 installSignalHandlers :: IO () -> IO ()
-installSignalHandlers shutdown =
-  for_ [Signals.sigINT, Signals.sigTERM] $ \signal ->
-    Signals.installHandler signal handler Nothing
-  where
-    handler = Signals.CatchOnce shutdown
+installSignalHandlers shutdown = do
+  mainThreadId <- myThreadId
+  -- Track Ctrl+C attempts
+  interruptRef <- newIORef False
+
+  let safeShutdown = do
+        -- Timeout after 30 seconds to ensure we don't hang
+        void $ Timeout.timeout (30 * 1000 * 1000) shutdown
+
+  -- Handle SIGTERM (systemd): always shutdown and exit immediately
+  _ <-
+    Signals.installHandler
+      Signals.sigTERM
+      ( Signals.Catch $ do
+          safeShutdown
+          throwTo mainThreadId ExitSuccess
+      )
+      Nothing
+
+  -- Handle SIGINT (interactive Ctrl+C): first attempt initiates shutdown, second exits
+  _ <-
+    Signals.installHandler
+      Signals.sigINT
+      ( Signals.Catch $ do
+          isSecondInterrupt <- liftIO $ atomicModifyIORef' interruptRef (True,)
+          if isSecondInterrupt
+            then throwTo mainThreadId ExitSuccess
+            else safeShutdown
+      )
+      Nothing
+
+  return ()
 
 registerAgent :: Agent -> WSS.AgentInformation -> IO ()
 registerAgent Agent {agentState, withLog} agentInformation = do
diff --git a/test/NixConfSpec.hs b/test/NixConfSpec.hs
--- a/test/NixConfSpec.hs
+++ b/test/NixConfSpec.hs
@@ -197,19 +197,28 @@
         NixConf.write conf
         readFile confPath `shouldReturn` confContents
 
-    it "resolves includes" $ do
+    -- Test that missing optional includes do not throw errors
+    it "resolves required and optional includes" $ do
       withTempDirectory "/tmp" "nixconf" $ \temp -> do
         let confPath = temp </> "nix.conf"
-            subConfPath = temp </> "sub.conf"
-            confContents = "include " <> toS subConfPath <> "\n"
-            parsedConfContents = NixConf [Include (RequiredInclude (toS subConfPath))]
-        writeFile confPath confContents
-        writeFile subConfPath realExample
+            requiredConfPath = temp </> "required.conf"
+            optionalConfPath = temp </> "optional.conf"
+            parsedConfContents =
+              NixConf
+                [ Include (RequiredInclude (toS requiredConfPath)),
+                  Include (OptionalInclude (toS optionalConfPath))
+                ]
+        writeFile confPath $
+          unlines
+            [ "include " <> toS requiredConfPath <> "\n",
+              "!include " <> toS optionalConfPath <> "\n"
+            ]
+        writeFile requiredConfPath realExample
 
         Just conf <- NixConf.read (Custom temp)
         NixConf.resolveIncludes conf
           `shouldReturn` [ NixConfSource confPath parsedConfContents,
-                           NixConfSource subConfPath parsedRealExample
+                           NixConfSource requiredConfPath parsedRealExample
                          ]
 
     it "detects cycles" $ do
