View Active Connections
Track active connections and the versions users run as a deployment rolls out.
Connection tracking only works with sse or ws update strategies. It does not support polling or external adapters (Pusher/Ably).
With connectionTracking: true, each tab keeps its own transport, routes, and statistics subscription.
Shared push ownership applies only when connection tracking is off.
Setup
Enable connection tracking in your Nuxt config using connectionTracking:
export default defineNuxtConfig({
skewProtection: {
connectionTracking: true,
routeTracking: true, // optional: track which routes users are viewing
ipTracking: true, // optional: track user IP addresses
}
})Limitations
- Single instance only: Stats are per-server process. With horizontal scaling, each instance only sees its own connections.
- SSE/WS only: Does not work with
pollingstrategy or external adapters (Pusher/Ably). - Bot traffic excluded: When you install
@nuxtjs/robots, the module automatically excludes bot/crawler traffic from connection counts. See Bot Traffic Filtering.
IP Tracking
IP tracking requires explicit opt-in via ipTracking: true due to privacy considerations. When enabled, the module extracts IP addresses from request headers in this order:
cf-connecting-ip(Cloudflare)x-forwarded-for(proxies/load balancers)x-real-ip(nginx)
Authorization
The server only sends stats to connections that pass authorization. Implement the skew:authorize-stats hook in a server plugin.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
// Works with nuxt-auth-utils
const session = await getUserSession(event)
if (session?.user?.role === 'admin') {
authorize()
}
})
})Cloudflare with nuxt-auth-utils. For WebSocket connections (e.g., with cloudflare-durable preset), the event object contains only { headers: Headers }. Unseal the session cookie manually:
import { unsealSession } from 'h3'
const ADMIN_EMAILS = ['admin@example.com']
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:authorize-stats', async ({ event, authorize }) => {
// Try getUserSession first (works for SSE/POST)
const session = await getUserSession(event).catch(() => {
// WebSocket events lack the H3 context required by getUserSession.
return null
})
if (session?.user?.email && ADMIN_EMAILS.includes(session.user.email)) {
authorize()
return
}
// For WebSocket, manually unseal the session cookie
if (event?.headers instanceof Headers) {
const cookieHeader = event.headers.get('cookie') || ''
const match = cookieHeader.match(/nuxt-session=([^;]+)/)
if (match?.[1]) {
const config = useRuntimeConfig()
const password = config.session?.password || process.env.NUXT_SESSION_PASSWORD || ''
if (password) {
const unsealed = await unsealSession(null as any, { password }, decodeURIComponent(match[1])).catch(() => {
// Invalid or expired cookies represent an unauthenticated connection.
return null
})
// Note: unsealSession returns { data: { user } }, not { user } directly
const email = unsealed?.data?.user?.email
if (email && ADMIN_EMAILS.includes(email)) {
authorize()
}
}
}
}
})
})getUserSession() returns { user: { email } } while unsealSession() returns { data: { user: { email } } }.Using the Composable
The useActiveConnections() composable provides reactive access to connection statistics:
<script setup lang="ts">
import { useActiveConnections } from '#imports'
const { total, versions, authorized } = useActiveConnections()
</script>
<template>
<div v-if="authorized">
<p>Active users: {{ total }}</p>
<ul>
<li v-for="(count, version) in versions" :key="version">
{{ version.slice(0, 8) }}: {{ count }} users
</li>
</ul>
</div>
<div v-else>
Not authorized for stats
</div>
</template>Return values:
| Property | Type | Description |
|---|---|---|
total | ComputedRef<number> | Total active connections |
versions | ComputedRef<Record<string, number>> | Map of buildId → connection count |
routes | ComputedRef<Record<string, number>> | Map of route → connection count (requires routeTracking) |
connections | ComputedRef<ConnectionInfo[]> | Individual connections with id, version, route, and ip (requires ipTracking for IP) |
yourId | ComputedRef<string | undefined> | Your connection ID to identify yourself in the list |
authorized | ComputedRef<boolean | null> | Whether the connection is authorized for stats (null while pending) |
Example: Admin Dashboard
<script setup lang="ts">
const { total, versions, authorized } = useActiveConnections()
const currentBuildId = useRuntimeConfig().app.buildId
const stats = computed(() => {
const current = versions.value[currentBuildId] || 0
const outdated = total.value - current
return {
total: total.value,
current,
outdated,
percentUpToDate: total.value ? Math.round((current / total.value) * 100) : 100
}
})
</script>
<template>
<div v-if="authorized" class="grid grid-cols-4 gap-4">
<div class="stat">
<span class="label">Total Users</span>
<span class="value">{{ stats.total }}</span>
</div>
<div class="stat">
<span class="label">On Latest</span>
<span class="value text-green">{{ stats.current }}</span>
</div>
<div class="stat">
<span class="label">On Old Version</span>
<span class="value text-orange">{{ stats.outdated }}</span>
</div>
<div class="stat">
<span class="label">Rollout Progress</span>
<span class="value">{{ stats.percentUpToDate }}%</span>
</div>
</div>
<div v-else>
Not authorized for live stats
</div>
</template>Custom Behavior with Hooks
Use hooks to handle connection messages and lifecycle events directly.
Client-Side: skew:message
Listen to all messages from the SSE/WebSocket connection:
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hooks.hook('skew:message', (message) => {
if (message.type === 'stats') {
// Custom handling
console.log('Connections:', message.total)
console.log('Versions:', message.versions)
}
})
})Server-Side: Connection Lifecycle
Build custom tracking logic with Nitro hooks:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('skew:connection:open', ({ id, version, route, ip, send }) => {
console.log(`Client ${id} connected on ${version} from ${ip}`)
// Send custom welcome message
send({ type: 'welcome', serverTime: Date.now() })
})
nitroApp.hooks.hook('skew:connection:close', ({ id }) => {
console.log(`Client ${id} disconnected`)
})
})See Nuxt Hooks and Nitro Hooks for full documentation.