Every distributed team eventually hits the same wall: you pick a protocol because it promises open federation, end-to-end encryption, or lightweight routing, but your daily workflow breaks in ways the spec never mentioned. The problem isn't the protocol itself—it's that we compare feature lists instead of state machines. This guide reframes distributed presence protocols as workflow ledgers: each message, status update, or access change is an entry that must be reconciled across nodes. We'll compare three major protocols—Matrix, ActivityPub, and XMPP—not by their marketing, but by how they handle the ledger of presence events that matter for real team collaboration.
If you're evaluating a protocol for a distributed incident response team, a federated community platform, or a multi-site project management tool, the same questions apply: How does the protocol buffer offline messages? Who owns the conflict resolution? What happens when two nodes disagree on who's online? By the end, you'll have a decision framework rooted in workflow traces, not spec sheets.
Why the Ledger Metaphor Matters Now
Distributed systems have always dealt with partial failure and inconsistent state. But three trends make the workflow-ledger view urgent. First, the shift from centralised SaaS to federated and peer-to-peer tools means teams no longer share a single server that guarantees ordering. Second, real-time collaboration expectations—presence indicators, typing notifications, instant message delivery—force protocols to resolve state conflicts in milliseconds, not minutes. Third, security requirements like end-to-end encryption add another layer: if two clients disagree on a room's membership list, encrypted messages may become undeliverable.
Consider a typical scenario: a security operations team spread across three time zones, using a mix of Matrix for chat, ActivityPub for status broadcasts, and a legacy XMPP bridge for automated alerts. When an incident triggers, the on-call engineer updates their presence to 'busy', which should suppress non-critical notifications. Under Matrix, the presence change is recorded as a state event in the room's event graph. Under ActivityPub, it's an activity object sent to followers. Under XMPP, it's a presence stanza broadcast to subscribed resources. Each approach creates a different ledger of who saw the change and when.
The catch is that none of these protocols guarantee total ordering across all participants. Matrix uses event IDs and server-side ordering within a room, but two servers may see events in different orders if network delays cause divergent arrival times. ActivityPub's delivery is best-effort; there's no consensus on the order in which activities are processed. XMPP relies on server-side message archives, but presence stanzas are ephemeral by default—if a client disconnects, its last presence may be lost. For a workflow ledger, this means you cannot assume every participant sees the same sequence of state changes. The practical consequence: a 'busy' status may be seen by some team members but not others, leading to missed escalations or duplicated alerts.
Teams often discover this gap only after an incident. One team I read about (names anonymised) used a Matrix bridge for their PagerDuty alerts. The bridge posted incident updates as Matrix messages, but the presence system was separate: the bridge didn't update its own presence based on who was on-call. When the on-call engineer changed, the old person's presence remained 'online' in the room, and other team members continued pinging them directly. The workflow ledger had two inconsistent sources of truth: the on-call schedule (external) and the Matrix presence state (internal). The fix required a custom bot that reconciled presence with the schedule, but it added latency and a single point of failure.
This example shows why the ledger view is practical: it forces you to map every state change that matters to your workflow—presence, message delivery, read receipts, membership changes—and ask whether the protocol guarantees that all participants converge on the same ledger. If not, you need compensating mechanisms: idempotent actions, conflict detection, or human reconciliation steps.
The stakes are higher when encryption is involved. Matrix's Olm/Megolm protocol relies on a shared room state to derive encryption keys. If two clients disagree on the room's member list, they may encrypt messages for a key the other client doesn't have, causing undecryptable messages. ActivityPub doesn't mandate end-to-end encryption, but some implementations use per-activity encryption; if the activity's audience list diverges, recipients may get unreadable payloads. XMPP's OMEMO encryption similarly depends on a shared device list. In all three cases, ledger consistency directly affects functional correctness.
Core Idea: Presence as a State Machine
Think of presence as a state machine with at least four states: offline, online, busy, and away. Each state transition is an event in the ledger. The protocol's job is to propagate that event to all interested parties and, ideally, to ensure that everyone sees the same sequence of transitions. But real-world presence is more complex: a user may have multiple devices, each with its own presence (e.g., phone online, laptop away). The protocol must merge these into a single 'user presence' or expose per-device states.
Matrix handles this through the concept of 'presentity'—a user's presence is a state event in the global user directory, updated via PUT /_matrix/client/v3/presence/{userId}/status. The event includes a 'presence' field (online, offline, unavailable) and an optional status message. Each Matrix server stores the latest presence for its local users and pushes changes to remote servers via federation. The ledger is append-only: each presence change creates a new event with a timestamp and event ID. Clients receive the latest state via sync, but they may miss intermediate states if they reconnect after multiple changes—they only see the final state.
ActivityPub models presence indirectly through activities. A user's status is not a first-class object; instead, you might send an Update activity to the user's followers, setting a property like `status: 'busy'`. The follower's server decides how to interpret and display this. There is no standard 'presence' vocabulary, so implementations vary. Some use the `as:status` property from ActivityStreams; others use custom extensions. The ledger is a stream of activities, but there's no guarantee of order across servers—each server processes activities in the order it receives them, which may differ.
XMPP defines presence via the `
The key difference is how each protocol handles history. Matrix keeps every presence event in the room's event graph (if presence is in a room context) or in the user's presence stream. ActivityPub's activities are stored in the user's outbox and inbox, but there's no requirement to retain them. XMPP's presence is transient unless archived via Message Archive Management (MAM) or custom logging. For workflow auditing—like proving who was online when an incident started—Matrix provides a clear ledger; ActivityPub leaves it to implementation; XMPP requires explicit archiving.
Another dimension is scope of delivery. Matrix presence is visible to any user who shares a room with the target user, or explicitly subscribes. ActivityPub presence (as an activity) is sent to followers, which may be a subset of the user's connections. XMPP presence is sent to roster subscribers, which are typically bidirectional friends. For a team workflow, you often want presence visible to all team members, not just direct connections. Matrix's room-based visibility is natural for team channels; XMPP requires everyone to be in each other's rosters; ActivityPub may need a group actor to forward presence to all members.
We can summarise the ledger properties in a comparison table:
| Property | Matrix | ActivityPub | XMPP |
|---|---|---|---|
| State history | Full event graph | Activity stream (retention varies) | Ephemeral (unless archived) |
| Ordering guarantee | Per-room causal order | None across servers | Per-server order |
| Delivery scope | Room members or subscribers | Followers | Roster subscribers |
| Encryption impact | State divergence breaks key agreement | No standard E2EE | OMEMO requires device list sync |
This comparison reveals that no single protocol dominates across all workflow needs. Matrix offers the richest ledger for auditing and causal ordering, but its reliance on room state makes it brittle if federation lags. ActivityPub is simplest for broadcast-style presence but lacks ordering and history guarantees. XMPP is lightweight and deterministic per server, but its ephemeral nature and roster-based scope limit workflow visibility.
How It Works Under the Hood: Federation, Buffering, and Conflict Resolution
To understand how each protocol behaves in practice, we need to look at three mechanisms: federation (how servers exchange ledger entries), buffering (how offline state is handled), and conflict resolution (what happens when two servers disagree).
Federation in Matrix
Matrix uses a server-to-server API based on HTTP and JSON. When a user changes presence, their homeserver sends a PUT request to all remote servers that share a room with that user. The remote server validates the event ID and stores it in its own event graph. If the remote server is unreachable, the sending server retries with exponential backoff, up to a configurable limit (default 72 hours). This means presence changes can be delayed significantly if a server is down. During that delay, the remote server's users see stale presence. Matrix's federation protocol does not support ordering across servers; each server applies events in the order it receives them, which may differ. However, within a single server, events are ordered by the server's clock and event ID.
Matrix's buffering for offline users works at the client level: when a client reconnects, it performs a sync request that includes all new events since its last sync token, including presence changes. The server retains the latest presence for each user, so the client will see the current state, but intermediate transitions are lost unless the client logs them locally. For a workflow ledger, this means you can reconstruct the final state, but not the exact sequence of changes that occurred while offline.
Federation in ActivityPub
ActivityPub federation is simpler: servers send activities (JSON-LD objects) to each other's inboxes via HTTP POST. There is no built-in retry mechanism; implementations may queue failed deliveries and retry, but the spec doesn't mandate it. This means presence updates (sent as Update activities) can be lost if the target server is down. ActivityPub also lacks a global ordering mechanism; each server processes activities in arrival order, and there's no way to detect missing activities unless the sender includes a `prev` pointer (not standard).
Buffering in ActivityPub is implementation-dependent. Some servers (like Mastodon) store activities in the inbox and deliver them to clients via streaming API; others (like Pleroma) use a simple REST model. If a client is offline, it may miss activities unless the server retains them for a period (Mastodon keeps activities for 30 days by default). However, presence is not a first-class concept, so there's no guarantee that a client will receive the latest status on reconnect—it depends on whether the server sent an Update activity at the time of the change.
Federation in XMPP
XMPP federation uses XML stanzas over persistent TCP connections (or BOSH/WebSocket). Servers route stanzas based on the domain part of the JID. Presence stanzas are broadcast to all subscribed servers, which then deliver to local clients. If a remote server is unreachable, the sending server may queue stanzas for a limited time (typically a few minutes) before bouncing them with an error. XMPP's presence is ephemeral: when a client disconnects, its server sends an unavailable stanza to all subscribers. If the connection drops without a proper disconnect, the remote server may detect the timeout (default 60 seconds) and send unavailable on behalf of the disconnected client.
Buffering in XMPP is not standard for presence; clients use Message Archive Management (XEP-0313) for message history, but presence is not archived by default. Some servers (like ejabberd) offer mod_presence_cache to store last presence, but this is not universal. For workflow auditing, you would need to log presence stanzas externally, e.g., via a component that subscribes to all presence.
Conflict Resolution
Conflicts arise when two servers have different views of the same ledger entry. In Matrix, the most common conflict is room state divergence: two servers may have different versions of the room state due to network partitions. Matrix resolves this via state resolution algorithms (v1, v2, v3) that compute a consistent state given a set of conflicting events. However, presence is not part of room state; it's a separate stream, so conflicts are resolved by last-writer-wins (LWW) based on timestamp. If two servers send different presence for the same user at nearly the same time, the server with the later timestamp wins. This can cause flapping if clocks are not synchronised.
ActivityPub has no built-in conflict resolution. If two servers send conflicting Update activities for the same user, the receiving server may simply apply the last one received, or ignore duplicates—there's no standard. This can lead to inconsistent presence across the federation.
XMPP avoids most conflicts by design: presence is per-client, and a user's server is the authoritative source for that user's presence. If a client sends a presence stanza, the server broadcasts it; there's no mechanism for another server to override it. Conflicts can occur if a user is logged in from multiple devices and each sends different presence (e.g., one says 'away', another says 'online'). The server may merge these according to priority (XMPP priority field), but the merging logic is implementation-specific. The default is to use the highest-priority resource's presence; if priorities are equal, the last one received wins.
Worked Example: Incident Response Across Three Protocols
Let's walk through a concrete scenario: a five-person incident response team uses a distributed presence system to coordinate during a security breach. The team members are Alice (on-call), Bob (escalation), Charlie (subject matter expert), Diana (manager), and Eve (observer). The workflow requires that when Alice goes 'busy', Bob is automatically notified and becomes the primary responder. If Bob is also busy, Charlie gets notified, and so on. The presence system must deliver changes within 10 seconds, and the escalation history must be auditable for post-incident review.
We'll compare how each protocol handles a sequence of presence changes: Alice changes from 'online' to 'busy' at T=0. Bob changes from 'online' to 'busy' at T=5 (because he was already handling another incident). Charlie is 'online'. Diana and Eve are 'away'.
Matrix Implementation
In Matrix, the team shares a room called '#incident:example.org'. Each member's presence is visible in the room's member list. Alice sets her presence to 'busy' via the client API. Her homeserver sends a presence update to all other homeservers that have members in the room. Bob's client receives the sync and sees Alice's status change. Bob's client is configured to automatically set his presence to 'busy' if Alice is busy and he is the next in line. This triggers a new presence update from Bob. Within seconds, Charlie's client sees both changes. The ledger shows: T=0: Alice→busy; T=5: Bob→busy. The escalation logic runs on Bob's client, which then sends a direct message to Charlie with a notification. Charlie's client sees the DM and his presence remains 'online'. The audit trail is the room's event graph: presence changes are state events with timestamps and event IDs. The team can export the room's history to get a complete timeline.
Potential issues: if Alice's homeserver is momentarily unreachable, Bob's server may not receive the update until later, causing a delay. If the escalation logic runs on the client, it depends on the client being online; if Bob's client is offline during the change, he won't auto-set his presence. Matrix's sync ensures Bob's client will eventually see the change when it reconnects, but the escalation may be delayed.
ActivityPub Implementation
In ActivityPub, each team member has an actor (e.g., @[email protected]). They follow each other, and there's a group actor @[email protected] that all members follow. Alice sends an Update activity to her followers, setting `as:status` to 'busy'. The activity is delivered to Bob's inbox (since Bob follows Alice) and to the group actor's inbox. Bob's server processes the activity and updates Bob's view of Alice's status. Bob's client, upon receiving the activity via streaming API, triggers his own Update activity to set his status to 'busy'. However, the group actor does not automatically escalate; an external service would need to listen to the group's inbox and trigger notifications. The ledger is the stream of activities in each user's inbox. The timeline is not globally ordered; Bob's server may process Alice's activity before or after Charlie's server, depending on network latency. For auditing, you would need to collect activities from all inboxes and sort by the activity's `published` time, but clock skew may cause misordering.
Issues: ActivityPub does not guarantee delivery; if Bob's server is down, Alice's update is lost. There is no standard retry. The escalation logic must be implemented as a separate service (e.g., a bot that follows the group actor and sends notifications). The bot would need to maintain its own state machine, effectively building a custom ledger on top of ActivityPub's activity stream. This adds complexity and a single point of failure.
XMPP Implementation
In XMPP, each team member has a JID (e.g., [email protected]). They are all in each other's rosters. Alice sends a `
Potential issues: if Bob's client disconnects before sending its own presence, the escalation fails silently. XMPP's roster-based delivery means everyone sees everyone's presence; there's no concept of 'group visibility' unless you use Multi-User Chat (MUC) and presence in a MUC room, which is possible but adds complexity. The escalation logic runs on the client, which may not be reliable if the client is not running.
Edge Cases and Exceptions
No protocol handles every edge case gracefully. Here are three common failure modes that teams encounter.
Split-Brain Federation
When two servers cannot communicate for an extended period, they diverge. In Matrix, this can lead to room state conflicts that require manual resolution. For presence, split-brain means one server sees Alice as 'online' (because it never received the 'busy' update), while another sees her as 'busy'. The team members connected to different servers will have inconsistent escalation triggers. Matrix's state resolution can fix room state, but presence is not resolved—it's LWW, so when the partition heals, the later timestamp wins, which may be the wrong one. For example, if Alice went 'busy' before the partition and 'online' after, the 'online' timestamp is later, so all servers will converge to 'online', even though Alice intended to stay 'busy'. This can cause missed escalations.
ActivityPub has no mechanism to reconcile divergent views; each server keeps its own copy of the actor's profile. If Alice's server goes down during a presence update, some followers may never get it. When the server comes back, it does not re-send lost activities. The only way to recover is for Alice to manually re-send the update after the server is back, which is error-prone.
XMPP's presence is ephemeral, so split-brain is less of an issue: when the partition heals, the next presence stanza from Alice will update all servers. However, if Alice is offline during the partition, her last presence (e.g., 'online') remains in the remote servers' caches indefinitely. When she comes back, she sends a new presence, which corrects the state. But if she went 'busy' and then disconnected before the partition, the 'busy' state is lost on servers that never received it—they will see her as 'online' after she disconnects (because they send unavailable on timeout). This is actually correct for XMPP: unavailable replaces the previous state. But if the team relies on 'busy' to trigger escalation, the state is lost when she disconnects, even if she intended to remain busy.
Multi-Device Presence Merging
Users often have multiple devices: phone, laptop, tablet. Each device may have a different presence. Matrix merges per-device presence into a single user presence by taking the most 'available' state (e.g., if any device is 'online', user is 'online'; if all are 'away', user is 'away'). This is simple but can be misleading: a user may be 'online' on their phone but not actively monitoring the team channel. For escalation, you may want to know if the user is 'online' on their primary device. Matrix does not expose per-device presence to other users; it's aggregated.
ActivityPub does not have a standard for multi-device; each device could be a separate actor, or a single actor with multiple endpoints. Most implementations assume one actor per user. This simplifies presence but doesn't reflect real-world usage.
XMPP handles multi-device natively: each device is a resource with its own JID (e.g., [email protected]/laptop). Presence stanzas include the resource. Other clients can see per-resource presence. The server can also compute a 'best' presence based on priority. This gives fine-grained control: Bob can see that Alice is 'online' on her phone but 'away' on her laptop, and decide to escalate based on the laptop's presence (since that's where she runs the incident tools). However, this requires clients to expose resource information, which may be too detailed for some workflows.
Offline Message Buffering and Presence
When a user is offline, their presence is typically 'offline'. But what if the team needs to know that a user was 'busy' at the time of an incident, even though they are now offline? Matrix retains the last presence state, so if Alice was 'busy' before going offline, her presence will appear as 'busy' until she comes back and changes it, or until the server times out (some Matrix servers expire presence after a period). This can be misleading: Alice may be offline, but her presence still shows 'busy'. The team may think she is still handling the incident, when in fact she has left. ActivityPub similarly retains the last activity; XMPP's ephemeral nature means offline users show as 'unavailable', which is honest but loses the history of their last active state.
For auditing, you need to record the state at specific timestamps, not just the latest. Matrix's event history provides this; ActivityPub's activity stream may have it if the server retains activities; XMPP requires custom logging.
Limits of the Approach: When the Ledger Metaphor Breaks Down
The workflow-ledger view is powerful, but it has limits. First, it assumes that a single ledger per team is desirable. In practice, teams use multiple tools: Slack for chat, email for formal communication, Jira for tickets. Each tool has its own presence system. Trying to unify them into one ledger adds complexity and may violate data locality. The ledger metaphor works best within a single protocol ecosystem, not across them.
Second, the ledger assumes eventual consistency is acceptable. For incident response, you may need strong consistency: when Alice goes 'busy', Bob must know immediately, not after a federation delay. Matrix's eventual consistency can cause windows where two team members see different presence. For time-critical workflows, you may need a centralised presence server that bypasses federation, which defeats the purpose of a distributed protocol.
Third, the ledger approach doesn't address human factors: presence is a social signal, not a deterministic state. A user may be 'online' but not responsive; 'busy' may mean 'do not disturb' or 'working on something else'. The protocol cannot capture intent. Teams often supplement presence with custom status messages or 'available' flags, which add another layer of state that may not be synchronised.
Fourth, the ledger is only as good as its retention policy. Matrix retains presence events indefinitely, which raises privacy concerns: you can reconstruct when a user was online over months. ActivityPub's retention varies; XMPP's ephemeral nature is privacy-friendly but loses audit trail. Teams must decide on a retention policy that balances audit needs with privacy.
Finally, the metaphor assumes that all participants are honest and trustworthy. In a federated system, a malicious server could forge presence events. Matrix has some protection via event signing, but it's not foolproof. ActivityPub has no standard authentication for activities beyond HTTP signatures, which are optional. XMPP uses server-to-server authentication (SASL), but a rogue server could send false presence. For security-critical workflows, you need additional verification, like out-of-band confirmation of presence changes.
Given these limits, the workflow-ledger view is best used as a diagnostic tool, not a design blueprint. Use it to trace where state can diverge and what compensating mechanisms you need. For each protocol, ask: what is the authoritative source for this state? How long does it take for a change to propagate? What happens if a server is down? What is the retention policy? The answers will guide your implementation choices.
If you're building a new distributed presence system, consider starting with Matrix for its rich event graph and federation, but add a fallback mechanism for time-critical escalations (e.g., a centralised WebSocket that bypasses federation for high-priority status changes). For lightweight, privacy-focused workflows, XMPP with custom logging may be simpler. ActivityPub is best for broadcast-style presence where history and ordering are not critical—like a public 'away' message for a social media account.
Ultimately, no protocol is perfect for every workflow. The ledger metaphor helps you compare them on a level playing field: not by their features, but by how they handle the state changes that your team depends on. Document your workflow's state machine, map it to the protocol's ledger, and test with real network conditions. That's the only way to know if the protocol works for you.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!