Matrix Presence v2 Logo Is Presence Fixed Yet?

Is presence broken now?

by L Veneris, with love, on 2026-09-02

If you’ve read any of the Presence v2 proposals, you may have noticed they begin by discussing the state of presence as it is in the Matrix protocol today. Rather shocking stuff, I know. What you may not know, however, is just how big of a deal these problems — and our solutions to them — actually are. In this post, we’ll take a brief journey through the situation the Presence v2 initiative is up against, and how effective implementing MSC4532: Revised Social Presence in particular might be.

Like the ones you unwrap?

Before we begin, you may want a refresher on the details of presence in Matrix, or perhaps in general. You may skip ahead if you’re confident in the subject. Otherwise, depending on your social platforms of choice, it is likely you already have some experience of presence.

Presence information can be found almost anywhere direct messaging can. Your favourite programs may tell you when your friend is available, or when they were last available, or that Emily is away, and they may even allow you to set status messages to let others know what you’re up to. All of these are examples of presence, named for their purpose of letting users of a social service know whether each other are available to talk or what they might be doing.

Within Matrix specifically, presence currently has a few components you need to be aware of:

These come together to represent all of the aforementioned kinds of presence information.

How bad can it possibly be?

Let’s see!

Observe Figures 1 through 3, containing various runtime statistics gathered from a Matrix server with 2 users. Can you guess when presence was enabled?

Screenshot of a resource graph demonstrating a notably unstable increase in CPU usage, from 0% to an inconsistent 10%. Screenshot of a resource graph demonstrating a notably unstable increase in open file descriptors, from just under 250 overall to spikes over 1000 from federation sender threads. Screenshot of a resource graph demonstrating a notably unstable increase in federated EDUs, from below 20Hz to peaks of 60-80Hz.

If you said the 7th of August (or the 8th of July if the date format snuck past you), congratulations. If you remain unconvinced, observe Figure 4, showing the updates sent by the same server after one user sent a single message.

All of these metrics together indicate an increase in the work the server is doing to share information with other servers. Presence, as it is now, can quickly become overwhelming to servers of any size. If 2 users show up on the graphs, imagine the costs incurred to resource-constrained or larger instances.

In numbers

While visuals are fun, we’re here for data. For today’s assignment, we have 313,553 federated presence updates to look at, all collected over the course of a few days. If you prefer independent work, fear not; there’ll be a section at the end of this post with guidance on collecting a presence dataset with Caddy and using our tool to process it.

Our processed dataset gives us the following information, showing which fields were changed compared to the previous presence update in the sequence and how many occurrences of that pattern there were.

Updated fields Quantity Proportion of total
currently_active 0 0.00%
presence, status_msg 0 0.00%
status_msg, currently_active 0 0.00%
presence, status_msg, currently_active 0 0.00%
status_msg 2 0.00%
presence, currently_active 9 0.00%
last_active_ago, status_msg, currently_active 10 0.00%
last_active_ago, presence, status_msg 13 0.00%
presence 15 0.00%
(empty) 343 0.11%
last_active_ago, status_msg 618 0.20%
last_active_ago, currently_active 1716 0.55%
last_active_ago, presence, status_msg, currently_active 2047 0.65%
last_active_ago, presence 31761 10.13%
last_active_ago, presence, currently_active 99038 31.59%
last_active_ago 177981 56.76%

Before we begin, it is worth mentioning how this system is supposed to work. When a user is not active, currently_active should be turned off, and last_active_ago is updated to how many milliseconds ago the user changed their presence state or interacted with a room. When a user is active, currently_active should be turned on so last_active_ago no longer has to be updated. In practice, these behaviours are bugged in many implementations, including a long-standing bug that causes one implementation to ship malformed presence updates out to the masses. The several years these bugs have been left untouched for is a strong indication of the dust accumulating on the presence module of the specification.

With that in mind, what does the data tell us? We can see a few things:

Won’t somebody help us?

I’m glad you asked. Now we’ve seen the state of presence in the wild, how about we see what Revised Social Presence could do for us? The most relevant changes here are the removal of last_active_ago and currently_active, which leaves us just with the user’s actual state. Let’s apply these changes to get an idea.

Updated fields Quantity Proportion of total
status_msg 630 0.2%
presence, status_msg 2060 0.66%
presence 130823 41.72%
(empty) 180040 57.42%

Now we’ve simplified the system to remove opportunities for broken behaviour in currently_active and last_active_ago, can we go further? Revised Social Presence removes the need for timed rebroadcasting altogether, so we can remove the last row of unchanged information.

Updated fields Quantity Proportion of total
status_msg 630 0.47%
presence, status_msg 2060 1.54%
presence 130823 97.99%

All presence updates now contain actual presence information, as they are intended to, and we’ve dropped more than half (57%) of the traffic in the process. You may be wondering at this point if this comes at the cost of any information; surely dropping half of all broadcasts entails removing features. I am delighted to inform you it does not. Aside from the disconcerting millisecond-accuracy data on when you last sent a message, which is now simply based on when you last appeared to be online, the system conveys all the same information as before. In fact, it manages to make these improvements while adding a busy state and making statuses extendable for future developments.

What have we done?

Finally, note that all of these changes apply to a remainder. MSC4495: Selective Presence will drastically reduce the amount of uninterested parties receiving presence in the first place. Choosing between letting the whole world know your business and nobody at all is simply not practical, so the added fidelity grants more privacy for users and lower compute costs for server operators. Everyone is a winner on this fine evening.

In conclusion, Revised Social Presence, as a counterpart to Selective Presence, is estimated to reduce the federation burden of the presence left standing by more than 50% in update volume. We can achieve all of this while not only transmitting the same information, but adding a busy state so you can let people know when you’re unavailable, improving the privacy of last seen timers without compromising user experience, and other goodies you can find in the proposal.

Thank you for reading, and we hope you’ll experience the benefits of this work soon enough.

Can I see for myself?

This guide uses Caddy to obtain Matrix federation transaction logs and process them, but you may adapt this setup for your preferred reverse proxy.

In the Caddy server block that reverse proxies traffic to your Matrix server, add the following log configuration.

matrix.example.tld {
    log {
        output file /var/log/caddy/presence.log {
            roll_size 512MiB
            roll_keep 5
            roll_keep_for 14d
        }
        format filter {
            request>headers delete
            request>tls delete
            bytes_read delete
            resp_headers delete
            user_id delete
        }
    }

    log_append req_body {http.request.body}
    log_append resp_body {http.response.body}

    @notsync not path /_matrix/federation/v1/send*
    log_skip @notsync

    # Your other directives and reverse_proxy line
    # ...
}

Now, download our Python library migrations.py, and run the following instructions in a Python interpreter from the same directory.

>>> import migrations
>>> import pathlib
>>> transactions = migrations.load_caddy(pathlib.Path("presence.log"))

Next, prepare the transactions for analysis by going through the formats in order.

Format 0 is the base for processed logs, grouping all transactions by their origin servers and deduplicating them.

>>> transactions = migrations.caddy_to_0(transactions)

Format 1 takes this a step further, flattening out the transactions into sequences of individual presence updates grouped by origin server and user.

>>> transactions = migrations.apply_1(transactions)

Finally, Format 2 converts the grouped sequence of presence updates for a given user into a series of differences, containing only the information that changed since the last update.

>>> transactions = migrations.apply_2(transactions)

The final schema of this data is as follows.

{
    "<server-name-uuid>": {
        "<user-uuid>": [
            {
                "edu": [
                    ["<property-name>", "<value>"],
                    // example: ["presence", "online"]
                    // ...
                ],
                "origin_server_ts": 0,
                "received_ts": 0,
                "request_uuid": "<request-uuid>"
            }
        ]
    }
}

Now, you may run the available analyses:

Our own dataset is available for download as a prepared collection of Format 1 transactions, in case you would like to check the results featured in this post.


  1. This number does not occur in the table because the dataset has to account for entries that are assumed to be unset when the stream starts. This means the first update of every user’s presence stream will contain all four fields, for the purpose of calculating the differences between each update and the next update. You may verify it independently as seen in the guide.