Detecting Slow Connections and Network Issues in Real-Time with Stimulus

When building Hey Cuis, a real-time quiz game, we needed a reliable way to inform players about connection problems before they affect gameplay. Nobody wants to lose a game because their internet hiccupped, and worse, not even knowing why.

This article explains how we implemented a simple but effective connection monitoring system using Rails and Stimulus.

The Problem

Real-time games are susceptible to network issues. A slow connection can cause:

  • Delayed player actions
  • Desynchronized game state
  • Frustrating user experience
  • Players blaming the game instead of their connection

We needed to detect two scenarios:

  1. Slow connections: The network is working, but responses are taking too long
  2. Complete disconnection: No network connectivity at all

The Solution

Our approach is straightforward: regularly ping our server and measure response time. Based on the latency, we show an appropriate warning banner.

Backend: A Simple Health Check Endpoint

First, we created a lightweight endpoint that does nothing but respond with a 200 OK:

# app/controllers/healthcheck_controller.rb
class HealthcheckController < ApplicationController
  def up
    head :ok
  end
end

This endpoint is intentionally minimal — no database queries, no authentication, no business logic. We want to measure network latency, not server processing time.

Frontend: Monitoring with Stimulus

The monitoring logic lives in a Stimulus controller that runs continuously in the background:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ['banner', 'title', 'description']
  static values = {
    checkInterval: { type: Number, default: 10000 },
    threshold: { type: Number, default: 1000 },
    slowTitle: { type: String, default: '' },
    slowDescription: { type: String, default: '' },
    offlineTitle: { type: String, default: '' },
    offlineDescription: { type: String, default: '' }
  }

  connect() {
    this.currentState = 'good'
    this.checkConnection()
    this.intervalId = setInterval(
      () => this.checkConnection(), 
      this.checkIntervalValue
    )
  }

  disconnect() {
    clearInterval(this.intervalId)
  }

  async checkConnection() {
    const start = performance.now()
    
    try {
      const response = await fetch('/up', {
        method: 'HEAD',
        cache: 'no-store'
      })
      
      const latency = performance.now() - start
      
      if (response.ok) {
        this.updateConnectionStatus(latency)
      }
    } catch (error) {
      this.showOfflineStatus()
    }
  }

  updateConnectionStatus(latency) {
    const isSlowConnection = latency > this.thresholdValue
    
    if (isSlowConnection && this.currentState !== 'slow') {
      this.showBanner('slow')
      this.currentState = 'slow'
    } else if (!isSlowConnection && this.currentState !== 'good') {
      this.hideBanner()
      this.currentState = 'good'
    }
  }

  showOfflineStatus() {
    if (this.currentState !== 'offline') {
      this.showBanner('offline')
      this.currentState = 'offline'
    }
  }

  showBanner(type) {
    this.bannerTarget.classList.remove('hidden')
    
    if (type === 'slow') {
      this.bannerTarget.dataset.currentState = 'slow'
      this.titleTarget.textContent = this.slowTitleValue
      this.descriptionTarget.textContent = this.slowDescriptionValue
    } else if (type === 'offline') {
      this.bannerTarget.dataset.currentState = 'offline'
      this.titleTarget.textContent = this.offlineTitleValue
      this.descriptionTarget.textContent = this.offlineDescriptionValue
    }
  }

  hideBanner() {
    this.bannerTarget.classList.add('hidden')
  }
}

How It Works

1. Regular Polling: Every 10 seconds, we make a HEAD request to /up and measure the round-trip time using performance.now().

2. Latency Threshold: If the response takes longer than 1 second (our threshold), we consider the connection slow.

3. State Management: We track three states — goodslow, and offline—and only update the UI when the state changes, preventing banner flickering.

4. Error Handling: Network errors (caught exceptions) indicate complete disconnection.

The UI Component

The banner uses data attributes to change its appearance based on the connection state:

<div data-controller="connection-monitor"
     data-connection-monitor-slow-title-value="<%= t('.connection.slow.title') %>"
     data-connection-monitor-slow-description-value="<%= t('.connection.slow.description') %>"
     data-connection-monitor-offline-title-value="<%= t('.connection.offline.title') %>"
     data-connection-monitor-offline-description-value="<%= t('.connection.offline.description') %>">
  
  <div class="fixed bottom-4 left-1/2 -translate-x-1/2 
              data-[current-state=slow]:bg-amber-500 
              data-[current-state=offline]:bg-red-500 
              rounded-2xl shadow-xl hidden"
       data-connection-monitor-target="banner">
    
    <div class="flex items-center gap-3 px-4 py-3">
      <%= icon_tag(name: 'exclamation-triangle', variant: :solid) %>
      
      <div>
        <p class="text-sm font-semibold text-white" 
           data-connection-monitor-target="title"></p>
        <p class="text-xs text-white/90" 
           data-connection-monitor-target="description"></p>
      </div>
    </div>
  </div>
</div>

The banner is amber for slow connections and red for complete disconnection, making the severity immediately clear.

Configuration Choices

We chose these values based on our game’s requirements:

  • Check interval: 10 seconds — Frequent enough to detect issues quickly without overwhelming the server
  • Latency threshold: 1 second — A reasonable limit for real-time interactions
  • HEAD requests — Minimize bandwidth usage since we only care about response time

These values are configurable through Stimulus values, allowing you to adjust them based on your application’s specific needs.

Conclusion

This simple monitoring system has significantly improved our players’ experience. When connection issues occur, users now understand what’s happening instead of assuming the game is broken.

The implementation is straightforward, requires minimal code, and can be adapted to any Rails application where connection quality matters. Whether you’re building a real-time game, collaborative editor, or any application sensitive to latency, monitoring connection health proactively helps users understand their experience better.

How would you approach this problem? What improvements or alternatives would you consider?