Back to Home

Titanium: messenger backend on Erlang

The article dissects the evolution of the Titanium backend for a custom messenger: abandoning XMPP/FastAPI in favor of Erlang/OTP. Describes pts-synchronization, sharding, benchmarks up to 5k RPS. For senior developers.

How to build a scalable messenger backend on Erlang
Advertisement 728x90

Titanium: Scalable Backend Core for Custom Messengers — From Prototype to Production

Developers building custom messengers often start with XMPP/ejabberd or Matrix/Synapse. These platforms provide ready-made transport, federation, and E2E encryption. But customizing for modern UX hits roadblocks.

XMPP with ejabberd on Erlang is compact and load-resistant, but:

  • XML stanzas add parsing overhead.
  • MUC rooms don't scale linearly: CPU spikes with participant growth.
  • Partial XEP extensions lead to race conditions in business logic.

Matrix is powerful, but its layered architecture makes changes tricky: Synapse on Python struggles with GIL in real-time tasks.

Google AdInline article slot

Hybrid setups—like ejabberd for transport + FastAPI for API/OTP/reactions—cause split-brain state, XML<->JSON conversions, and extra queues. The single source of truth gets blurred across components.

Takeaway: It's simpler to build a backend from scratch for full control over state and performance.

Titanium v1: FastAPI Under Load

The first Titanium version on FastAPI with WebSocket handled core features: chats, UINs, media, pts-sync. The prototype sustained 500 RPS in a 1000-user scenario with 120 msg/s, 40 media/min, and 7% disconnects.

Google AdInline article slot

RPS was measured on full paths: send -> delivery -> ack -> sync. This covered:

  • Access rights validation.
  • pts updates (message points for delta-sync).
  • Real-time WebSocket pushes.
  • Retries on flaky networks.
  • Update logs for offline clients.

Issues emerged at scale:

  • GIL throttled WebSocket concurrency.
  • PostgreSQL locked up on high contention in pts tables.
  • Media storage needed temporary links, not permanent ones.

Titanium Architecture: Where the Truth Lives

Titanium defines strict state management rules:

Google AdInline article slot
  • Server as source of truth: Never trust clients; all changes via API.
  • Pts mechanism: Delta-sync via message points, Telegram-style.
  • Update log: Immutable log for state replay.
  • Distributed state: Sharding by UIN, no sticky sessions.
  • Media: Temporary S3-style signatures, 24h TTL.

Key components:

  • Gateway: WebSocket + HTTP/2, rate limiting, JWT.
  • Core: Business logic, pts, ACLs (by UIN/groups).
  • Storage: PostgreSQL (states) + Redis (sessions/queues) + S3 (media).
  • Workers: Celery/RQ for async tasks (media processing, bots).

Pts-sync example (pseudocode):

% Client requests delta
handle_get_updates(ReqPts) ->
  Updates = db:fetch_updates_after(ReqPts, UserId),
  NewPts = lists:max([U#update.pts || U <- Updates]),
  {reply, #{updates => Updates, pts => NewPts}}.

Switching to Erlang/OTP: Scalability and Resilience

Python couldn't handle 10k concurrent WebSockets. Erlang/OTP fixed it:

  • Actor model: Each UIN gets a supervisor tree with gen_server.
  • VM hot-reload with zero downtime.
  • Built-in Mnesia/ETS for low-latency storage.
  • Cowboy for WebSocket scaling to 1M+ connections per node.

Benchmarks:

| Scenario | FastAPI (Python) | Titanium (Erlang) |

|----------|-------------------|--------------------|

| 1k users, 120 msg/s | 500 RPS | 5k RPS |

| 10k WS | 20% CPU | 5% CPU |

| Media 100/min | 2s latency | 200ms |

Gen_server session example:

-module(session).
-export([start_link/1, handle_message/2]).

handle_call({send_message, Msg}, _From, State) ->
  case validate_rights(Msg) of
    ok ->
      NewPts = State#state.pts + 1,
      save_update(Msg#update{pts=NewPts}),
      broadcast_to_peers(Msg),
      {reply, ok, State#state{pts=NewPts}};
    {error, Reason} -> {reply, {error, Reason}, State}
  end.

Group Chats and Edge Cases

Groups are a beast. Ditching MUC for:

  • Sharded rooms: Leader by hash(UIN_list), followers replicate deltas.
  • Presence: ETS for online/offline, pubsub on reconnect.
  • Bad networks: Optimistic send + server ACK, gap-filling from logs.

Handled cases:

  • Duplicate sends (idempotency by msg_id).
  • Packet reordering (timestamp + seq).
  • Sleep/wakeup tabs (long-poll fallback).
  • Bot webhooks with retries (dead letter queue).

Key Takeaways

  • Titanium isn't an MTProto clone—it's a lean real-time stack: pts, logs, sharding.
  • Erlang/OTP delivers 10x RPS at 1/4 CPU vs Python.
  • Server truth prevents client-side inconsistencies.
  • Media/bots offloaded async; core focuses on delivery.
  • Scale: 3 nodes handle 50k users, 1k msg/s sans federation.

— Editorial Team

Advertisement 728x90

Read Next