diff --git a/package-lock.json b/package-lock.json
index 7bde0ea94085c433103a8812ecd2c4ccab63fa47..7401d2c0a710845ce76492dbf01fbddc0c8c657c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6870,9 +6870,9 @@
       }
     },
     "source-map-support": {
-      "version": "0.5.15",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.15.tgz",
-      "integrity": "sha512-wYF5aX1J0+V51BDT3Om7uXNn0ct2FWiV4bvwiGVefxkm+1S1o5jsecE5lb2U28DDblzxzxeIDbTVpXHI9D/9hA==",
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
       "dev": true,
       "requires": {
         "buffer-from": "^1.0.0",
diff --git a/src/liowebrtc b/src/liowebrtc
index 7e94603dca2fbffe844fe5ee4f3361e9c361e7cc..ce4a2ebe160804ed84f7b6fc3bd10c91e766bdcd 160000
--- a/src/liowebrtc
+++ b/src/liowebrtc
@@ -1 +1 @@
-Subproject commit 7e94603dca2fbffe844fe5ee4f3361e9c361e7cc
+Subproject commit ce4a2ebe160804ed84f7b6fc3bd10c91e766bdcd
diff --git a/src/room.js b/src/room.js
index ae28294abeeb45dd0831723c01b891702d423516..e92d017c25210413e573b203f17c9ef4abf41d41 100644
--- a/src/room.js
+++ b/src/room.js
@@ -81,8 +81,22 @@ class Room extends EventTarget {
         name: "webrtc",
         url: "/",
         room: this.name,
+        handshake: {
+          initial: 100,
+          interval: 500,
+        },
+        heartbeat: {
+          interval: 500,
+          minimum: 1000,
+          timeout: 10000,
+        },
         onUserEvent: (event) => {
-          if (event.action == "userID") {
+          if (event.action == "userConnection") {
+            const { quality } = event
+            this.dispatchEvent(
+              new CustomEvent("userConnection", { detail: quality }),
+            )
+          } else if (event.action == "userID") {
             const { id } = event
             this.ownID = id
             this.dispatchEvent(new CustomEvent("allocateOwnID", { detail: id }))
diff --git a/src/y-webrtc/index.js b/src/y-webrtc/index.js
index dc2f7308dea14be711daa359420416b6b98c0062..97ff1539eb5a0b9b6a228204578d874ccac408ee 100644
--- a/src/y-webrtc/index.js
+++ b/src/y-webrtc/index.js
@@ -14,6 +14,20 @@ function extend(Y) {
       super(y, options)
       this.webrtcOptions = options
 
+      this.webrtcOptions.handshake = this.webrtcOptions.handshake || {}
+      this.webrtcOptions.handshake.initial =
+        this.webrtcOptions.handshake.initial || 100
+      this.webrtcOptions.handshake.interval =
+        this.webrtcOptions.handshake.interval || 500
+
+      this.webrtcOptions.heartbeat = this.webrtcOptions.heartbeat || {}
+      this.webrtcOptions.heartbeat.interval =
+        this.webrtcOptions.heartbeat.interval || 500
+      this.webrtcOptions.heartbeat.minimum =
+        this.webrtcOptions.heartbeat.minimum || 1000
+      this.webrtcOptions.heartbeat.timeout =
+        this.webrtcOptions.heartbeat.timeout || 10000
+
       this.queue = new Worker("js/queue.js")
       this.queue.onmessage = (event) => {
         const method = event.data.method
@@ -96,11 +110,13 @@ function extend(Y) {
         console.log("TODO: LEFT ROOM")
       })
 
-      this.webrtc.on("channelError", (a, b, c, d) => console.log(a, b, c, d))
+      this.webrtc.on("channelError", (a, b, c, d) =>
+        console.log("TODO: CHANNEL ERROR", a, b, c, d),
+      )
 
       this.webrtc.on("channelOpen", (dataChannel, peer) => {
         this.checkAndEnsureUser()
-        console.log(dataChannel)
+
         // Start a handshake to ensure both sides are able to use the channel
         function handshake(peer) {
           const _peer = this.webrtc.getPeerById(peer.id)
@@ -121,10 +137,16 @@ function extend(Y) {
             message: "tw",
           })
 
-          setTimeout(handshake.bind(this, peer), 500)
+          setTimeout(
+            handshake.bind(this, peer),
+            this.webrtcOptions.handshake.interval,
+          )
         }
 
-        setTimeout(handshake.bind(this, peer), 100)
+        setTimeout(
+          handshake.bind(this, peer),
+          this.webrtcOptions.handshake.initial,
+        )
       })
 
       this.webrtc.on("receivedPeerData", (type, message, peer) => {
@@ -166,17 +188,14 @@ function extend(Y) {
         return
       }
 
-      console.log(
-        this.webrtc
-          .getPeerById(uid)
-          .getStats(null)
-          .then((stats) => stats.forEach((report) => console.log(report))),
-      )
-
-      const health = {}
+      const health = {
+        lastStatsResolved: true,
+        lastReceivedBytes: 0,
+        lastReceivedTimestamp: Date.now(),
+      }
       health.cb = setInterval(
         this.heartbeat.bind(this, this.webrtc.getPeerById(uid), health),
-        500,
+        this.webrtcOptions.heartbeat.interval,
       )
 
       this.peers.set(uid, health)
@@ -193,26 +212,67 @@ function extend(Y) {
         return
       }
 
+      if (!health.lastStatsResolved) {
+        return peer.end(true)
+      }
+      health.lastStatsResolved = false
+
       const self = this
 
-      // TODO: Check which stats are supported by different browsers
-      // TODO: Check massive renegotiation on reconnect
       peer.getStats(null).then((stats) => {
-        stats.forEach((report) => {
-          if (report.type == "candidate-pair" && report.selected) {
-            if (Date.now() - report.lastPacketReceivedTimestamp > 10000) {
-              return peer.end(true)
-            }
+        health.lastStatsResolved = true
 
-            if (Date.now() - report.lastPacketReceivedTimestamp > 500) {
+        let disconnect = true
+
+        stats.forEach((report) => {
+          if (
+            report.type == "candidate-pair" &&
+            report.bytesSent > 0 &&
+            report.bytesReceived > 0 &&
+            report.writable
+          ) {
+            const timeSinceLastReceived =
+              Date.now() - health.lastReceivedTimestamp
+
+            if (report.bytesReceived != health.lastReceivedBytes) {
+              health.lastReceivedBytes = report.bytesReceived
+              health.lastReceivedTimestamp = Date.now()
+            } else if (
+              timeSinceLastReceived > self.webrtcOptions.heartbeat.timeout
+            ) {
+              return
+            } else if (
+              timeSinceLastReceived > self.webrtcOptions.heartbeat.interval
+            ) {
               self.queue.postMessage({
                 method: "send",
                 uid: peer.id,
                 channel: "heartbeat",
               })
             }
+
+            for (let f of this.userEventListeners) {
+              f({
+                action: "userConnection",
+                quality:
+                  1.0 -
+                  (self.webrtcOptions.heartbeat.timeout -
+                    Math.max(
+                      timeSinceLastReceived,
+                      self.webrtcOptions.heartbeat.minimum,
+                    )) /
+                    (self.webrtcOptions.heartbeat.timeout -
+                      self.webrtcOptions.heartbeat.minimum),
+              })
+            }
+
+            disconnect = false
           }
         })
+
+        if (disconnect) {
+          peer.end(true)
+        }
       })
     }