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 |
|
|
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 ( |
Built in or plugins |
Built in modules, plus MUC Light |
Built in |
Clustering |
Mnesia or SQL, |
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"
Source: Prosody documentation: mod_websocket, mod_bosh, mod_mam, mod_muc and mod_http_file_share.
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 |
|
BOSH with |
Prosody |
|
|
Openfire |
|
Enabled in the admin console; the Openfire install guide lists ports 7070 and 7443 for BOSH and WebSocket. |
MongooseIM |
|
BOSH is |
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
Originheader, but the browser does not block a cross-origin WebSocket by itself; the server has to. A request withOrigin: https://evil.example.testto the ejabberd endpoint above was accepted with101 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 needsAccess-Control-Allow-Originon those responses. Thecustom_headersentry above shows ejabberd; Prosody allows cross-domain requests by default. -
The host-meta file. A client that knows only
chat.example.testcan requesthttps://chat.example.test/.well-known/host-meta(or the.jsonvariant) 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 = falsein Prosody,access: registerrules in ejabberd) and create accounts from your application withejabberdctl registerorprosodyctl 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-OAUTH2mechanism lets a web application issue tokens (see the browser page).
Modules that Chatterbox needs
| Feature | ejabberd module | Prosody module |
|---|---|---|
Rooms (XEP-0045) |
|
|
Archive (XEP-0313) |
|
|
File upload (XEP-0363) |
|
|
Carbons (XEP-0280) |
|
|
Stream Management (XEP-0198) |
|
|
PEP (XEP-0163), avatars, OMEMO keys |
|
|
Push (XEP-0357) |
|
|
TURN discovery (XEP-0215) |
|
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@node1on 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
modulessection is strict: a typo or a module that is not part of the release (for examplemod_csi, which does not exist in 26.09) stops the server from starting. Read the log line. -
Two hosts, one name. The
Hostheader selects the virtual host for HTTP modules. Requesting host-meta withHost: localhostreturns an error when onlychat.example.testis configured. -
WebSocket works locally, fails behind nginx. Missing
UpgradeandConnectionheaders, or an idle timeout shorter than the XMPP ping interval. Prosody additionally needsconsider_websocket_secure. -
Cross-origin fetch of host-meta blocked. The response has no
Access-Control-Allow-Origin. -
Archive not recording. Check
default: always(ejabberd) ordefault_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.