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.

MyRoom.ts
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

OptionTypeDefaultDescription
timeoutMsnumberrequiredMilliseconds of inactivity after which a client is kicked.
scanIntervalMsnumbermin(timeoutMs / 4, 5000)Milliseconds between scans. Smaller values kick sooner past the deadline.
closeCodenumber1000WebSocket close code sent to the kicked client.
reasonstring'kicked'WebSocket close reason forwarded to the SDK’s onLeave.
isExempt(client: Client) => booleanOptional predicate to exempt a client (e.g., admins, spectators). Return true to keep the client immune.
onKick(client: Client, idleMs: number) => voidCalled 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:

client.ts
room.onLeave((code, reason) => {
  if (code === 1000 && reason === "kicked") {
    showAfkScreen();
  }
});

Examples

Exempt admins

MyRoom.ts
plugins = definePlugins([
  new IdleKickPlugin({
    timeoutMs: 30_000,
    isExempt: (client) => client.auth?.role === "admin",
  }),
]);

Broadcast a notice before kicking

MyRoom.ts
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 40114999. Colyseus reserves 40004010. 4010 in particular triggers the SDK’s auto-reconnect, the opposite of a kick:

MyRoom.ts
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.
  • _lastMessageTime is 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