IdleKickPlugin
Auto-disconnects clients that haven’t sent a message in timeoutMs. Useful for kicking AFK players from competitive rooms, freeing seats in matchmaking lobbies, or shedding zombie connections that the WebSocket layer still considers alive.
Footprint: one clock.setInterval per room, zero per-message work, zero per-client state. Activity is read from client._lastMessageTime, which the room already maintains for rate-limiting.
Installation
The plugin ships with the colyseus package: no extra dependency to install.
import { Room, definePlugins } from "colyseus";
import { IdleKickPlugin } from "colyseus/plugins/idle-kick";
export class MyRoom extends Room {
plugins = definePlugins([
new IdleKickPlugin({ timeoutMs: 60_000 }),
]);
}Options
| Option | Type | Default | Description |
|---|---|---|---|
timeoutMs | number | required | Milliseconds of inactivity after which a client is kicked. |
scanIntervalMs | number | min(timeoutMs / 4, 5000) | Milliseconds between scans. Smaller values kick sooner past the deadline. |
closeCode | number | 1000 | WebSocket close code sent to the kicked client. |
reason | string | 'kicked' | WebSocket close reason forwarded to the SDK’s onLeave. |
isExempt | (client: Client) => boolean | — | Optional predicate to exempt a client (e.g., admins, spectators). Return true to keep the client immune. |
onKick | (client: Client, idleMs: number) => void | — | Called just before a client is kicked. Use to log, broadcast a notice, or persist analytics. |
Handling the kick on the client
By default the plugin closes with WS code 1000 (“normal closure”) and reason 'kicked'. The SDK treats 1000 as a final leave (no auto-reconnect attempt) and forwards the reason string to room.onLeave:
room.onLeave((code, reason) => {
if (code === 1000 && reason === "kicked") {
showAfkScreen();
}
});Examples
Exempt admins
plugins = definePlugins([
new IdleKickPlugin({
timeoutMs: 30_000,
isExempt: (client) => client.auth?.role === "admin",
}),
]);Broadcast a notice before kicking
plugins = definePlugins([
new IdleKickPlugin({
timeoutMs: 60_000,
onKick: (client, idleMs) => {
this.broadcast("player-afk", {
sessionId: client.sessionId,
idleMs,
});
},
}),
]);The arrow function above works because class-field initializers bind this lexically: inside the arrow, this is the room. A plain function () {} callback would instead get the plugin instance as this, and this.broadcast would fail. Stick to arrows here.
Custom close code
If you’d rather signal a domain-specific reason (e.g., to differentiate from a generic 1000 disconnect), use a custom code in the range 4011–4999. Colyseus reserves 4000–4010. 4010 in particular triggers the SDK’s auto-reconnect, the opposite of a kick:
plugins = definePlugins([
new IdleKickPlugin({
timeoutMs: 60_000,
closeCode: 4040,
reason: "afk",
}),
]);Caveats
- Any inbound frame refreshes activity: including SDK keepalive PINGs. This behavior is by design: an idle client whose socket is still healthy is preferable to a zombie connection that gets force-closed. If you need stricter “user-payload-only” semantics, manage your own timestamps from your message handlers.
_lastMessageTimeis updated at most once per second by the rate-limiter, so timing precision is ±1 second. For an idle timeout measured in seconds-to-minutes, this is far below the noise floor.
See Also
- Room Plugins: overview of the plugin system
- Reconnection Handling:
onDrop/allowReconnectionflow for unintentional disconnects