
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:
- Slow connections: The network is working, but responses are taking too long
- 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 — good
, slow
, 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?