         initial-sleep 200
         sleep-multiplier 2
         maximum-sleep (* 15 1000)]
-    (loop [retry-sleep initial-sleep]
+    ;; Use a separate promise for ensuring the heartbeat stops when connection is closed.
+    ;; If a new connection is established, a new heartbeat thread will start.
+    (loop [retry-sleep initial-sleep
+           stop-heartbeat (promise)]
       (or (try+
             (log/debug (i18n/trs "Making connection to {0}" server))
             (ws/connect server
                         :client websocket-client
                         :on-connect (fn [session]
-                                      (log/debug (i18n/trs "WebSocket connected")))
+                                      (log/debug (i18n/trs "WebSocket connected, heartbeat task starting"))
+                                      (.start (Thread. (partial heartbeat websocket-client stop-heartbeat))))
                         :on-error (fn [error]
                                     (log/error error (i18n/trs "WebSocket error")))
                         :on-close (fn [code message]
                                     ;; Format error code as a string rather than a localized number, i.e. 1,234.
                                     (log/debug (i18n/trs "WebSocket closed {0} {1}" (str code) message))
+                                    (deliver stop-heartbeat true)
                                     (let [{:keys [should-stop websocket-connection]} client]
                                       ;; Ensure disconnect state is immediately registered as connecting.
                                       (log/debug (i18n/trs "Sleeping for up to {0} ms to retry" retry-sleep))

  (connected? [client] (-connected? client))
  (wait-for-connection [client timeout] (-wait-for-connection client timeout))
  (send! [client message] (-send! client message))
  (close [client] (-close client))
  (start-heartbeat-thread [client] (-start-heartbeat-thread client)))

(s/defn ^:private -connecting? :- s/Bool
  [client :- Client]

;; with the dash so they're not clashing with the versions defined by
;; ClientInterface
(declare -connecting? -connected?
         -wait-for-connection -send! -close)

(s/defrecord Client
  [server :- s/Str
