XMPP Servers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

An XMPP chat needs a server before it needs a client, and the browser clients of this section (Strophe.js, @xmpp/client and Converse.js) only need three things from it: a WebSocket endpoint that speaks RFC 7395, the multi-user chat and archive modules, and an HTTP upload service. This page compares the five servers most often used for that, runs one of them (ejabberd) in Docker Compose with exactly those features, shows the equivalent Prosody configuration, and lists the WebSocket details of the others. The protocol background is on XMPP Fundamentals.

Versions were checked on 2026-10-24: ejabberd 26.09 (released 2026-09-30, Docker image ghcr.io/processone/ejabberd:26.09), Prosody 13.0.7, Openfire 5.1.2 (2026-08-17), MongooseIM 6.10.0 (2026-10-15) and Tigase 8.4.3 (2026-10-04). The ejabberd configuration below is written for 26.09; the Prosody, Openfire and MongooseIM snippets follow their official documentation.

Servers compared

ejabberd Prosody Openfire MongooseIM Tigase

Language and licence

Erlang, GPL-2.0 with the OpenSSL exception

Lua, MIT

Java, Apache-2.0

Erlang, GPL-2.0

Java, AGPL-3.0

Typical size

Large

Small to medium; low memory

Small to large

Large, built for backends

Small to large

Administration

ejabberdctl, web admin, REST API, ejabberd.yml

prosodyctl, Lua configuration file

Web admin console, plugins

Configuration in TOML, GraphQL API

Web admin, configuration file

WebSocket and BOSH

Built in

Built in

Built in (WebSocket on the HTTP ports)

Built in

Built in

MUC, MAM, HTTP upload

Built in modules

Built in modules (muc, mam, http_file_share)

Built in or plugins

Built in modules, plus MUC Light

Built in

Clustering

Mnesia or SQL, join_cluster

None in core; scale by sharding hosts

Hazelcast clustering plugin

CETS or Mnesia

Cluster mode (see the Tigase documentation)

Distinctive feature

Scale, SQL and Redis backends, MQTT and SIP built in

Easy to read config, strong community modules

Friendly admin UI, plugin ecosystem

Inbox, MUC Light, push and mobile focus

Component model, multi-protocol

How to choose for Chatterbox:

  • Prosody is the lightest and the easiest to configure for a demo, a small team or a self-hosted community; its documentation is excellent, and it is the server inside Jitsi Meet (see Janus and Jitsi).

  • ejabberd is the default pick when a chat must grow: SQL storage, clustering and large rooms are first-class, and it exposes almost every feature through a single YAML file. The examples on this page use it.

  • Openfire suits teams that want a GUI and Java plugins, and an existing Java stack.

  • MongooseIM is aimed at products built on top of a messaging backend and mobile applications (inbox, push, MUC Light for lightweight rooms).

  • Tigase is a strong alternative when you want a Java server with a component architecture.

All five are listed on the XMPP Standards Foundation software pages with their Compliance Suite claims, which is the quickest way to check that a server supports the web and IM categories described on the fundamentals page.

A verified ejabberd setup

The Compose file runs one ejabberd container with its client and HTTP ports published on loopback only:

name: chatterbox-xmpp
services:
  ejabberd:
    image: ghcr.io/processone/ejabberd:26.09
    hostname: chat.example.test
    ports:
      - "127.0.0.1:5222:5222"   # XMPP over TCP
      - "127.0.0.1:5280:5280"   # HTTP: WebSocket, BOSH, upload, host-meta
    volumes:
      - ./ejabberd/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro

The configuration below is the one used for every example of this group. It enables WebSocket and BOSH, multi-user chat, the archive, HTTP upload, Stream Management, Carbons, PEP (for avatars, bookmarks and OMEMO keys), blocking and TURN credential discovery. The domain chat.example.test is reserved for documentation, and the secrets are placeholders.

hosts:
  - chat.example.test

loglevel: info

listen:
  - port: 5222
    ip: "::"
    module: ejabberd_c2s
    max_stanza_size: 262144
    starttls_required: false          # development only: no TLS on loopback
  - port: 5280
    ip: "::"
    module: ejabberd_http
    custom_headers:
      Access-Control-Allow-Origin: "http://localhost:8080"
      Vary: Origin
    request_handlers:
      /ws: ejabberd_http_ws           # RFC 7395
      /bosh: mod_bosh                 # XEP-0124 and XEP-0206 fallback
      /upload: mod_http_upload        # XEP-0363 PUT and GET
      /.well-known/host-meta: mod_host_meta
      /.well-known/host-meta.json: mod_host_meta

acl:
  local:
    user_regexp: ""                   # every account of this host

access_rules:
  local:
    allow: local
  register:
    allow: local
  muc_create:
    allow: local
  muc:
    allow: all

modules:
  mod_disco: {}
  mod_caps: {}
  mod_ping: {}
  mod_roster:
    versioning: true
  mod_carboncopy: {}                  # XEP-0280
  mod_stream_mgmt:                    # XEP-0198
    resume_timeout: 300
  mod_mam:                            # XEP-0313
    default: always
    assume_mam_usage: true
  mod_muc:                            # XEP-0045 on conference.chat.example.test
    access: muc
    access_create: muc_create
    default_room_options:
      mam: true
  mod_http_upload:                    # XEP-0363 on upload.chat.example.test
    put_url: http://localhost:5280/upload
    docroot: /tmp/upload
  mod_bosh: {}
  mod_host_meta:                      # XEP-0156 /.well-known/host-meta
    bosh_service_url: http://localhost:5280/bosh
    websocket_url: ws://localhost:5280/ws
  mod_pubsub:                         # PEP for avatars, bookmarks and OMEMO
    access_createnode: local
    plugins: [flat, pep]
  mod_stun_disco:                     # XEP-0215 TURN credentials
    credentials_lifetime: 12h
    secret: change-me-turn-secret
    services:
      - host: turn.example.test
        port: 3478
        type: stun
        transport: udp
        restricted: false
      - host: turn.example.test
        port: 3478
        type: turn
        transport: udp
        restricted: true
  mod_register:
    access: register
  mod_privacy: {}
  mod_blocking: {}                    # XEP-0191
  mod_vcard: {}
  mod_push: {}                        # XEP-0357

Source: ejabberd documentation: Listen options and ejabberd documentation: Modules. The module names are the ones in the 26.09 documentation; some modules depend on others (for example mod_pubsub needs mod_caps and mod_blocking needs mod_privacy), and ejabberd refuses to start with a clear message naming the missing one.

Create two test accounts (placeholder passwords) and check the endpoints:

docker compose up -d
docker compose exec ejabberd ejabberdctl register alice chat.example.test change-me-alice
docker compose exec ejabberd ejabberdctl register bob chat.example.test change-me-bob

# host-meta, as XEP-0156 clients request it (the Host header selects the virtual host)
curl -s -H "Host: chat.example.test" http://localhost:5280/.well-known/host-meta

# the WebSocket upgrade, with the xmpp subprotocol
curl -si --max-time 3 -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Protocol: xmpp" http://localhost:5280/ws

The host-meta request returned an XRD document with two links, urn:xmpp:alt-connections:xbosh pointing at http://localhost:5280/bosh and urn:xmpp:alt-connections:websocket pointing at ws://localhost:5280/ws; the upgrade request returned 101 Switching Protocols with Sec-Websocket-Protocol: xmpp. A client discovers these links with XEP-0156 (see XMPP in the Browser).

ejabberd logs a warning that mod_host_meta is enabled on a non-encrypted listener, which XEP-0156 disallows. That is acceptable on loopback; in production serve the listener over TLS (the tls option) or terminate TLS at a reverse proxy.

Remove everything when done: docker compose down -v.

The Prosody configuration

Prosody reads one Lua file, and HTTP services are modules. The equivalent of the ejabberd file, for Prosody 13, is:

admins = { "admin@chat.example.test" }

modules_enabled = {
  "roster"; "saslauth"; "disco"; "carbons"; "pep"; "blocklist"; "ping"; "vcard4"; "vcard_legacy";
  "smacks";            -- XEP-0198 Stream Management
  "csi_simple";        -- Client State Indication for mobile clients
  "mam";               -- XEP-0313
  "cloud_notify";      -- XEP-0357 push
  "bosh";              -- XEP-0124 and XEP-0206, served at /http-bind
  "websocket";         -- RFC 7395, served at /xmpp-websocket
  "http_altconnect";   -- XEP-0156 host-meta
}

allow_registration = false
authentication = "internal_hashed"
c2s_require_encryption = true

-- Behind a reverse proxy that terminates TLS, tell Prosody the connection is secure.
consider_websocket_secure = true
consider_bosh_secure = true

-- HTTP listener behind the proxy
http_ports = { 5280 }
http_interfaces = { "127.0.0.1" }

VirtualHost "chat.example.test"

Component "conference.chat.example.test" "muc"
  modules_enabled = { "muc_mam" }
  restrict_room_creation = "local"

Component "upload.chat.example.test" "http_file_share"
  http_file_share_size_limit = 10 * 1024 * 1024
  http_file_share_expires_after = "1 week"

Register users with prosodyctl register alice chat.example.test change-me-alice. Prosody serves WebSocket at /xmpp-websocket and BOSH at /http-bind. CORS is allowed by default for both, so a page served from another origin can connect; narrow it with Prosody’s HTTP CORS settings if the page origin is known. Prosody rejects a WebSocket connection that is not secure unless consider_websocket_secure is true, which is the typical symptom of a proxy that terminates TLS: the client connects, and the server closes the stream with a policy error until the option is set.

WebSocket endpoints per server

Server WebSocket path and port Notes

ejabberd

ejabberd_http_ws handler on the path you choose (/ws above), on an ejabberd_http listener

BOSH with mod_bosh. ejabberd_http listeners accept tls, custom_headers and request_handlers.

Prosody

/xmpp-websocket on the HTTP port (5280) and HTTPS port (5281)

consider_websocket_secure and consider_bosh_secure for proxies.

Openfire

/ws/ on the HTTP port 7070 (WS) and 7443 (WSS)

Enabled in the admin console; the Openfire install guide lists ports 7070 and 7443 for BOSH and WebSocket.

MongooseIM

mongoose_websocket_handler in a listen.http entry, path = "/ws-xmpp"

BOSH is mongoose_bosh_handler at /http-bind.

Tigase

Enabled in the Tigase connection manager configuration

See the Tigase documentation for the settings.

MongooseIM example:

[[listen.http]]
  port = 5280

  [[listen.http.handlers.mongoose_websocket_handler]]
    host = "_"
    path = "/ws-xmpp"

  [[listen.http.handlers.mongoose_bosh_handler]]
    host = "_"
    path = "/http-bind"

Origin, CORS and the host-meta file

Three browser-facing concerns apply to every server:

  • Origin checking. The WebSocket handshake carries the page’s Origin header, but the browser does not block a cross-origin WebSocket by itself; the server has to. A request with Origin: https://evil.example.test to the ejabberd endpoint above was accepted with 101 Switching Protocols, so the default configuration does not enforce an Origin allow-list. For Chatterbox, keep the XMPP HTTP port bound to loopback and put a reverse proxy in front of it that rejects other origins (see Security and Proxies, Load Balancers and Deployment). Note that an Origin check alone is not authentication: the login still needs SASL.

  • CORS for BOSH and host-meta. BOSH uses plain HTTP requests, and the host-meta lookup is a fetch(), so the page needs Access-Control-Allow-Origin on those responses. The custom_headers entry above shows ejabberd; Prosody allows cross-domain requests by default.

  • The host-meta file. A client that knows only chat.example.test can request https://chat.example.test/.well-known/host-meta (or the .json variant) and find the WebSocket and BOSH URLs (XEP-0156). When the XMPP server is not on the web server for the domain, serve a static file there that points to it:

    <?xml version="1.0" encoding="UTF-8"?>
    <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
      <Link rel="urn:xmpp:alt-connections:websocket" href="wss://xmpp.example.test/ws"/>
      <Link rel="urn:xmpp:alt-connections:xbosh" href="https://xmpp.example.test/bosh"/>
    </XRD>

Registration policy

Open registration is the biggest operational risk of a public XMPP server, because spam and abuse accounts are created automatically:

  • Keep registration closed (allow_registration = false in Prosody, access: register rules in ejabberd) and create accounts from your application with ejabberdctl register or prosodyctl register, an admin API or a custom authentication backend.

  • If registration must be open, put CAPTCHA, invitations or e-mail verification in front, and rate-limit registrations per IP.

  • Authenticate against an existing user database instead of creating XMPP-only accounts: ejabberd supports SQL, LDAP and an external script, Prosody has authentication modules, and ejabberd’s X-OAUTH2 mechanism lets a web application issue tokens (see the browser page).

Modules that Chatterbox needs

Feature ejabberd module Prosody module

Rooms (XEP-0045)

mod_muc

muc component

Archive (XEP-0313)

mod_mam

mam, and muc_mam for rooms

File upload (XEP-0363)

mod_http_upload

http_file_share component

Carbons (XEP-0280)

mod_carboncopy

carbons

Stream Management (XEP-0198)

mod_stream_mgmt

smacks

PEP (XEP-0163), avatars, OMEMO keys

mod_pubsub with the pep plugin

pep

Push (XEP-0357)

mod_push

cloud_notify

TURN discovery (XEP-0215)

mod_stun_disco

a TURN discovery module from the Prosody module list

For a production archive, move MAM to SQL (ejabberd warns that the default Mnesia storage is limited to 2 GB) and set a retention window, because the archive grows without bound otherwise.

Clustering and scaling

  • ejabberd clusters with Erlang distribution: start the nodes with the same cookie, run ejabberdctl join_cluster ejabberd@node1 on the new node, and put a shared SQL database behind MAM and rosters. Sessions are balanced across nodes by the load balancer; stanzas are routed between nodes transparently.

  • MongooseIM clusters with CETS (the newer option) or Mnesia, and scales horizontally for many connections.

  • Openfire clusters with the Hazelcast plugin and a shared database.

  • Prosody runs as a single process per host in its core and has no clustering in core; scale it by sharding hosts and rely on its small footprint for the first tens of thousands of users.

The WebSocket layer scales like any other long-lived connection: see Scaling WebSockets for file descriptors, load balancer idle timeouts and reconnection storms. Because an XMPP client can resume a session with Stream Management, a rolling restart of a cluster node is less disruptive than a restart of a stateless WebSocket server: clients on the node reconnect and send resume to any node that still holds the session, or authenticate again.

Common errors and pitfalls

  • A module name that does not exist. ejabberd’s modules section is strict: a typo or a module that is not part of the release (for example mod_csi, which does not exist in 26.09) stops the server from starting. Read the log line.

  • Two hosts, one name. The Host header selects the virtual host for HTTP modules. Requesting host-meta with Host: localhost returns an error when only chat.example.test is configured.

  • WebSocket works locally, fails behind nginx. Missing Upgrade and Connection headers, or an idle timeout shorter than the XMPP ping interval. Prosody additionally needs consider_websocket_secure.

  • Cross-origin fetch of host-meta blocked. The response has no Access-Control-Allow-Origin.

  • Archive not recording. Check default: always (ejabberd) or default_archive_policy (Prosody): by default some servers archive only for clients that have enabled it.

  • Treating a successful WebSocket handshake as authenticated. The upgrade is anonymous; authentication happens inside the stream.