       (notify)
       (loop []
         ;; getNotifications is not interruptible, so it will take up to `timeout` for this loop to exit
-        (let [notifications (.getNotifications pg-conn (.toMillis timeout))]
+        (let [notifications (try
+                              (.getNotifications pg-conn (.toMillis timeout))
+                              (catch PSQLException e
+                                ;; hide failures during shutdown; the database connection was closed while waiting for notifications
+                                (if (.isInterrupted (Thread/currentThread))
+                                  (log/info "Ignoring errors during shutdown:" (str e))
+                                  (throw e))))]
           (when-not (.isInterrupted (Thread/currentThread))
             (doseq [_ notifications]
               (notify))

           (.submit ^Callable (until-interrupted
                               (executors/safe-task
                                #(listen-for-gis-changes refresh-async!)))))
  :stop (.shutdownNow ^ExecutorService notified-refresh))

           (java.time Duration)
           (java.util UUID)
           (java.util.concurrent Executors ExecutorService TimeUnit)
           (org.postgresql PGConnection)
           (org.postgresql.util PSQLException)))

(defn- process-changes! [conn state had-changes?]
  (if-some [change (gis-db/next-unprocessed-change conn)]
