Asynchronous
Chat Server
Mojolicious
Web Development
Real-time Communication

Asynchronous Chat Server using Mojolicious

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Mojolicious is a strong fit for chat applications because WebSocket support is built into the framework and runs on a non-blocking event loop. You can start with a very small server, then add rooms, identity, validation, and horizontal scaling without rewriting the entire architecture. The critical skill is treating each connection as event-driven state rather than request-response HTTP.

Build a Minimal Non-Blocking Chat Endpoint

A practical starting point is one WebSocket route that registers clients and broadcasts incoming messages. This code runs asynchronously because handlers execute only when events arrive.

perl
1use Mojolicious::Lite -signatures;
2
3my %clients;
4my $next_id = 1;
5
6websocket '/chat' => sub ($c) {
7  my $id = $next_id++;
8  $clients{$id} = $c;
9
10  $c->on(message => sub ($c, $msg) {
11    for my $peer_id (keys %clients) {
12      next if $peer_id == $id;
13      $clients{$peer_id}->send("user-$id: $msg");
14    }
15  });
16
17  $c->on(finish => sub ($c, $code, $reason) {
18    delete $clients{$id};
19  });
20};
21
22app->start;

This is enough for local testing with two browser tabs. Even here, you should think about resource cleanup, because stale connections can accumulate quickly in long-running processes.

Use Structured Messages Instead of Raw Strings

Raw text messages are fine for demos but become brittle once you need typing indicators, system notices, and client metadata. JSON envelopes keep protocol evolution manageable.

perl
1use Mojo::JSON qw(decode_json encode_json);
2
3sub safe_decode {
4  my ($raw) = @_;
5  my $data = eval { decode_json($raw) };
6  return undef if !$data || ref $data ne 'HASH';
7  return $data;
8}
9
10$c->on(message => sub ($c, $raw) {
11  my $in = safe_decode($raw) or return;
12
13  my $payload = encode_json({
14    type => 'chat',
15    user => $in->{user} // 'guest',
16    text => $in->{text} // '',
17    ts   => time,
18  });
19
20  $_->send($payload) for values %clients;
21});

With an explicit type field, clients can route messages by behavior rather than brittle string parsing. That pays off once mobile and web clients diverge.

Add Rooms and Membership Rules

Global broadcast becomes noisy as usage grows. Room-level fanout is the usual next step. Track room members and broadcast only within the room.

perl
1my %rooms;
2
3websocket '/chat/:room' => sub ($c) {
4  my $room = $c->param('room');
5  my $id   = "$c";
6
7  $rooms{$room} //= {};
8  $rooms{$room}{$id} = $c;
9
10  $c->on(message => sub ($c, $msg) {
11    for my $peer (values %{ $rooms{$room} }) {
12      $peer->send($msg);
13    }
14  });
15
16  $c->on(finish => sub ($c, $code, $reason) {
17    delete $rooms{$room}{$id};
18    delete $rooms{$room} if !keys %{ $rooms{$room} };
19  });
20};

This model also gives you a clean place to enforce room permissions. For private channels, verify membership during connect and reject unauthorized joins early.

Reliability: Heartbeats, Limits, and Backpressure

Real-time services fail in production when connection health is ignored. Add these controls early:

  • heartbeat ping and timeout policy
  • maximum message size to prevent memory abuse
  • send rate limits per connection and per user
  • bounded queue strategy for slow consumers

A common backpressure approach is dropping or closing clients that cannot keep up rather than letting process memory grow unbounded.

Authentication and Identity Binding

Do not trust user names sent inside message payloads. Identity should come from validated credentials during the WebSocket handshake, then be attached to connection state.

Typical flow:

  1. Client sends token in query parameter or header.
  2. Server validates token and extracts user id.
  3. Server stores user id in connection stash.
  4. Outbound chat payload uses server-side identity.

This prevents simple impersonation by crafted client messages.

Scaling Past a Single Node

In-memory maps work only when all users connect to one process. For multi-instance deployment, add a broker such as Redis pub-sub.

  • local Mojolicious worker handles sockets
  • incoming socket messages are published to Redis topic per room
  • every worker subscribed to that topic pushes to local room sockets

This architecture keeps the WebSocket layer lightweight while broker handles cross-node propagation. It also enables rolling deploys without disconnecting everyone through one node.

Operability and Debugging

Track metrics that answer real operational questions:

  • active connections per node
  • connection churn rate
  • messages per second by room
  • dropped messages and close reasons
  • publish-to-delivery latency

Log connect and disconnect with stable user and room identifiers, then sample message-level logs to avoid noise. During incident response, this makes it possible to separate authentication issues from broker lag or client network problems.

Common Pitfalls

  • Keeping all state in one process and expecting horizontal scaling to work.
  • Broadcasting unvalidated raw payloads to all clients.
  • Failing to remove disconnected clients and leaking memory.
  • Ignoring message size limits and per-user rate limiting.
  • Trusting client-supplied identity instead of validated server-side identity.

Summary

  • Mojolicious provides a compact event-driven foundation for WebSocket chat.
  • JSON message envelopes make protocol growth predictable.
  • Room-scoped fanout is required for usable multi-channel chat behavior.
  • Production readiness depends on heartbeat checks, limits, and backpressure handling.
  • Identity must be validated at connect time and bound to connection context.
  • Multi-node chat requires pub-sub infrastructure, not only in-memory maps.

Course illustration
Course illustration

All Rights Reserved.