diff --git a/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala b/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala
index 4b2313b1e53b623e28b8dd5f1d6d8ff4462f87ea..c468c3d588ad1c4f7862ba75b93fece96b127b89 100644
--- a/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala
+++ b/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala
@@ -10,8 +10,6 @@ import scala.collection.mutable
 import scala.util.{Success, Failure, Try}
 import scala.concurrent.duration.Duration
 
-import cask.endpoints.WsChannelActor
-
 case class BroadcastException(ex: Throwable) extends Throwable
 
 /** Server-side apps definition and abstractions */
@@ -86,8 +84,7 @@ private[web] abstract class ServerApp:
           handleMessage(userId, ujson.read(data))
       }
 
-  def disconnect(userId: UserId, channel: WsChannelActor): Unit = instanceLock.synchronized:
-    recordActivity()
+  def disconnect(userId: UserId, channel: cask.endpoints.WsChannelActor): Unit = instanceLock.synchronized:
     channels(userId).remove(channel)
     if channels(userId).isEmpty then channels.remove(userId)
     println(f"[${appInfo.id}/$instanceId] client \"$userId\" disconnected")
@@ -96,11 +93,14 @@ private[web] abstract class ServerApp:
   def connectedClients: Seq[UserId] = instanceLock.synchronized:
     channels.keysIterator.toSeq
 
+  def hasClients = instanceLock.synchronized:
+    channels.nonEmpty
+
   def shutdown() = instanceLock.synchronized:
-     for channels <- channels.values
-         channel <- channels
-     do
-       channel.send(cask.Ws.Close())
+    for channels <- channels.values
+        channel <- channels
+    do
+      channel.send(cask.Ws.Close())
 
   /** Sends a message to a specific client. */
   private def send(userId: UserId)(message: ujson.Value): Unit = instanceLock.synchronized:
@@ -178,8 +178,6 @@ private[web] final class ClockDrivenStateMachineServerApp[E, S, V](
     val name = f"${appInfo.id}/$instanceId/clock"
     Clock(name, sm.clockPeriodMs)(handleClockTick)
 
-  clock.start()
-
   def handleClockTick: Unit = instanceLock.synchronized:
     try
       val event = Left(Tick(System.currentTimeMillis))
@@ -189,6 +187,17 @@ private[web] final class ClockDrivenStateMachineServerApp[E, S, V](
     catch ex =>
       println(f"[${appInfo.id}/$instanceId] Uncaught exception in clock action: $ex")
 
+  override def connect(userId: UserId)
+                      (implicit cc: castor.Context, log: cask.Logger)
+    : cask.WebsocketResult = instanceLock.synchronized:
+    if !hasClients then clock.start()
+    super.connect(userId)
+
+  override def disconnect(userId: UserId, channel: cask.endpoints.WsChannelActor)
+    : Unit = instanceLock.synchronized:
+    super.disconnect(userId, channel)
+    if !hasClients then clock.shutdown()
+
   override def shutdown(): Unit =
     clock.shutdown()
     super.shutdown()
diff --git a/jvm/src/main/scala/cs214/webapp/server/web/WebServer.scala b/jvm/src/main/scala/cs214/webapp/server/web/WebServer.scala
index 88e1e6118b056a8cd6ec1d0575fac05ac70a6795..69e96b5077c92891bfec207b0dbb1dd8094d20a6 100644
--- a/jvm/src/main/scala/cs214/webapp/server/web/WebServer.scala
+++ b/jvm/src/main/scala/cs214/webapp/server/web/WebServer.scala
@@ -60,12 +60,12 @@ object WebServer:
   private[web] def createInstance(appId: AppId, registeredUserIds: Seq[UserId]): InstanceId =
     withFreshInstanceId: instanceId =>
       instances(instanceId) = appDirectory(appId).init(instanceId, registeredUserIds)
-      println(f"[$appId] instance created $instanceId")
+      println(f"[$appId/$instanceId] instance created")
       instanceId
 
   private[web] def shutdownApp(instanceId: InstanceId): Unit =
     instances.remove(instanceId).map: app =>
       app.shutdown()
-      println(s"[${app.appInfo.id}][$instanceId] shut down")
+      println(f"[${app.appInfo.id}/$instanceId] instance shut down")
 
   GarbageCollector.start()