DEV Community

Cover image for Your Own Matrix Server: The Real Cost of Federating Synapse
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

Your Own Matrix Server: The Real Cost of Federating Synapse

I have been running my own mail server for years, and the one-sentence lesson from that job is this: installation is the cheapest part. One morning outbound delivery from one of my domains stopped. It was not a misconfiguration, and it was not a lack of resources. The reputation of the outbound IP had gone bad. The fix was not a bigger machine but moving traffic through a relay — repairing not the server itself, but the face it turns toward the federated world. In federated protocols, that is always where the bill comes from.

The same story applies to Matrix. You can bring up Synapse, the homeserver developed by Element, in a single evening. But what you have built is not an application; it is a commitment opened up to a federation. In this post I itemise that commitment line by line: name resolution, database, disk, identity layer, workers and version maintenance. And at the end I ask the real question — should you federate at all, or run an island?

One note on transparency up front: I do not operate Synapse in a production environment with thousands of users. This article is the result of reading today's official documentation against my own self-hosting history. Rather than inventing numbers, I prefer to write down what the docs actually say and flag where they will hurt you.

What Federation Buys, and What It Asks in Return

The promise of federation is lovely: your users live on your server, but they can join rooms on other servers around the world. What it asks in return is a cost most people fail to notice during setup — once your users join a room, the state of that room becomes partly your server's responsibility. The bigger the room, the heavier the bill.

Synapse ties this to a concrete number. The limit_remote_rooms option measures the "complexity" of a remote room before joining it, and the definition in the documentation is precise: complexity is the number of current state events in the room divided by 500. The default threshold is 1.0 — but the check itself is disabled by default. Out of the box, Synapse will happily try to join an enormous room even on a resource-constrained VPS.

limit_remote_rooms:
  enabled: true
  complexity: 0.5
  admins_can_join: true
Enter fullscreen mode Exit fullscreen mode

I would write those three lines early. On a small server, the first real slowdown almost always begins with the sentence "somebody joined a very crowded room". Putting a scale at the door is cheaper than doing database archaeology later.

The First Wall: Name Resolution and Delegation

Federation teaches its first lesson in DNS, and that lesson cannot be undone: server_name is embedded inside user identities. Once you have written @mustafa:example.com, that domain is part of every identity; you cannot later decide it should have been chat.example.com. So your first decision is strategic rather than technical: are you signing your apex domain, or a subdomain?

The second lesson is about ports. The documentation states it plainly: other servers will try to reach you via your server_name on port 8448. If you want that traffic on 443, you have two routes — .well-known delegation or an SRV record. Serving the .well-known file from Synapse itself is not the default; you have to switch it on:

serve_server_wellknown: true
Enter fullscreen mode Exit fullscreen mode

There is a trap here that the documentation calls out separately: this flag only works if https://<server_name> is already routed to Synapse. If you host Synapse on a subdomain such as synapse.example.com — which is the most common reason for needing delegation in the first place — you have to serve the file yourself from the web server on the apex domain.

The file's content is a single line of JSON: {"m.server": "<your.server.name>[:<port>]"}. If you omit the port, 8448 is assumed. The Synapse docs mark the SRV route as "generally not recommended", and the reasoning is sound: getting the TLS certificates right becomes difficult in that scenario.

If you do end up on SRV, beware — you will run into thousands of stale blog posts here. The Matrix server-server API specification uses the _matrix-fed._tcp record in its resolution steps, and that record was added in spec version v1.8. The older _matrix._tcp form is still read, but the specification explicitly marks those steps as deprecated, on the grounds that they use a service name not registered with IANA. For a new deployment, write _matrix-fed._tcp; most guides you find online will still show you the old one.

Diagram

The third lesson hides in the reverse proxy, and this is the genuinely sneaky one. Synapse's nginx example uses a location ~ ^(/_matrix|/_synapse/client) block, and the warning the docs underline is not to add a path — not even a single slash — after the port in proxy_pass. nginx canonicalises the URI at that point, and because federation requests are signed, signature verification then fails. The result: your server looks perfectly healthy to itself, everything works from a browser, and only federation quietly collapses. There is also client_max_body_size; nginx defaults to a small value, so unless you align it with Synapse's max_upload_size (which defaults to 50M), your users cannot upload files.

The Database: What Starting on SQLite Costs

Synapse starts up on SQLite, and that does not mean "you may stay on SQLite". The installation docs do not even bother being polite: SQLite is only acceptable for testing purposes, should not be used in a production server, and performs poorly especially when participating in large rooms. That is exactly why a dedicated migration tool (synapse_port_db) is maintained for the move.

When you do migrate, there is one line that will stop the server from starting at all if you get it wrong:

createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse_user synapse
Enter fullscreen mode Exit fullscreen mode

Without the correct encoding, Synapse cannot store UTF-8 strings; and if the collation and ctype values differ from what it expects, it refuses to start. There is an allow_unsafe_locale escape hatch, but the documentation is honest about the risk: enabling it may corrupt your database, and things get messier still if locale libraries are updated or replicas run different versions. My own position is never to open that hatch — fixing a database created with the wrong locale months later costs far more than getting it right on day one.

The migration itself carries a hidden clause too: the documentation notes that after porting, the data may take up 25% to 100% more space on disk. There is also a hard warning: under no circumstances should you VACUUM the SQLite database between runs of synapse_port_db. The reflex to "free up a bit of space while I'm here" turns into data inconsistency at exactly that point.

The Disk Fills Quietly: Media and History

The most annoying surprise of running your own server is usually not CPU but disk. Synapse has two separate sources of growth, and both default to "never delete anything".

The first is media. The media_retention option comes with two sub-options: local_media_lifetime and remote_media_lifetime. Both default to null — meaning that unless you configure them, neither the files your own users upload nor the remote media cached from federation will ever be purged. The remote part matters most: every image shared in a crowded room your user has joined can leave a copy on your disk.

media_retention:
  local_media_lifetime: 90d
  remote_media_lifetime: 14d
Enter fullscreen mode Exit fullscreen mode

Do not misread those two values in the usual way: they measure not the age of the media but the time since it was last accessed. For media that has never been accessed, the creation time is used instead. So a three-year-old file people still open is never purged, while yesterday's file that nobody touched falls in scope.

I would be aggressive on remote media and moderate on local. Deleting remote media is not an irreversible loss; it can be fetched again if needed. Local media is your own data, and you cannot treat it with the same nerve.

The second is message history. The retention block lets you define a server-level retention policy, and it too is disabled by default. Once enabled, Synapse periodically purges events past their lifetime and also filters events arriving over federation so that already-expired events are not stored again. In a corporate deployment this is less a disk optimisation than a compliance requirement — the most concrete way to put in writing how long you keep data.

There is also a change of direction on the media side that will surprise you if you follow an old guide: enable_authenticated_media is now on by default (it changed in 1.120). That means newly uploaded media returns 404 on the legacy unauthenticated endpoints (download and thumbnail under /_matrix/media/...). The docs also say the option will eventually be removed and become always-on. The critical detail: media uploaded before this option was switched on remains accessible over the legacy endpoint forever. So "I've moved to authenticated media" does not mean your historical files are covered.

And there is the other side of the coin, where the truly irreversible item on a federated server hides: the backup of that disk. Synapse's state lives in three separate places — the Postgres database, the media_store directory, and the signing key under signing_key_path. Restore the first two from backups taken at independent moments and you are left with media the database knows about but the disk does not have. The third is your server's federated identity: lose it and you have to fill in old_signing_keys by hand, with public keys gathered from other servers' memory of you. The documentation even provides a curl example that queries matrix.org for them — which sounds exactly as reassuring as a backup strategy as you would expect.

The Identity Layer Shifted Under Your Feet

This is the most current section of the article, and the one that will catch most people unprepared. Synapse's authentication is moving out of its own internal API and into a separate service: the Matrix Authentication Service (MAS). The configuration now has a stable matrix_authentication_service block:

matrix_authentication_service:
  enabled: true
  secret_path: /etc/synapse/mas.secret
  endpoint: http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

Note two details immediately: the stable integration landed in v1.136.0 and requires MAS 0.20.0 or later. The real line item, though, does not show up in the configuration — MAS is a separate process that wants its own PostgreSQL database for its state, and it comes with its own routes in the reverse proxy. Moving the identity layer out is not a config change; it is a second service you have to operate.

The critical point is that this is no longer an "experimental feature" — and the old path is gone. Synapse's upgrade notes state that support for MSC3861 Auth Delegation (experimental_features.msc3861) was dropped in v1.157.0, in favour of the stable integration. If you run MAS behind the experimental flag, an upgrade will stop you dead. A classic Friday-evening scenario for an administrator who runs apt upgrade without reading the notes.

The direction is clear: Element recommends MAS going forward, matrix.org migrated its own server to MAS in 2025, and a syn2mas migration tool exists for moving existing deployments. The practical consequence is that identity in Matrix is now a citizen of the OAuth 2.0 / OpenID Connect world. On the enterprise side that is actually good news — instead of keeping users in a separate password pool, you can hook them to your existing identity provider. I have written before about single sign-on for self-hosted services with Authentik; a MAS-based Synapse is becoming one of the easier pieces to fit into that picture.

Scaling: Workers and a Moving Contract

A single-process Synapse gets you to a point. Beyond it, the architecture changes: synapse.app.generic_worker processes take on sync requests, federation traffic and client APIs, while inter-process communication runs over Redis — both as the pub/sub channel that distributes the replication stream and as a shared cache. The main process opens an HTTP replication listener and declares itself in instance_map. Specific parts of the load are split out via stream_writers and federation_sender_instances. The reverse proxy no longer just says "pass this to Synapse"; it knows which endpoint goes to which worker, and a given user's sync requests need to land on the same worker consistently.

What I actually want to point out here is not the worker list itself, but that this configuration is a moving contract. A concrete example: the v1.152.0 upgrade notes say that deployments routing the /quarantine_media endpoints to a worker must also add that worker to the new quarantined_media_changes stream writer list; without it, quarantining media silently fails. On a single-process deployment you need to do nothing at all.

The practical rule that falls out of this: moving to a worker architecture buys capacity while enlarging the maintenance surface. One more document to read on every upgrade, one more mapping to keep in sync in the reverse proxy. If your user count does not demand it, staying single-process is not a shortcoming but a deliberate choice.

Federated, or an Island?

Now for the real question. If you are putting Matrix into a corporate environment, federation may not automatically be what you want. Synapse offers federation_domain_whitelist for exactly this: list some domains and you federate only with those; write an empty list ([]) and federation is off entirely — the documentation marks this as the recommended way to disable federation.

But do not skip the two caveats immediately below it. First, this is an application-layer restriction; you are advised to also firewall your federation listener. Second, and subtler: this option does not stop your server from joining rooms that contain servers not on the whitelist. The whitelist only creates a genuine "private federation" when a group of servers all whitelist each other. A list written unilaterally is not as sharp a boundary as you think.

If you are leaving federation open, review a few out-of-the-box defaults:

  • trusted_key_servers defaults to matrix.org, and Synapse prints a warning at startup because of it. You can silence the warning with suppress_key_server_warning, but decide first: do you trust a third party for key verification? In a private federation you can empty the list and request keys directly from the server that owns them — but there is a price: if Synapse cannot get the keys directly from that server, its events are rejected.
  • presence is enabled by default. Showing online status is a nice feature, but it generates constant federation traffic. You can disable it, or since 1.96.0 set it to "untracked".
  • url_preview_enabled is off by default, and if you enable it you must define url_preview_ip_range_blacklist. The reason is a classic SSRF scenario: a server that generates link previews is precisely a bot capable of making requests into your internal network on a user's behalf. The docs also state clearly that the URL-based blocklist is a usability feature, not a security one.
  • enable_registration is off by default, and if you enable it the docs recommend adding at least one of CAPTCHA, email verification or a registration token. An openly registrable Matrix server is a free door into the federation for spammers.

Version Maintenance: Updating a Fortnightly Project Once a Year

The last line item is operational discipline. Synapse is a fast-moving project, and faster than you probably assume: in the first eight and a half months of 2026, from 1.145.0 on 13 January to 1.159.0 on 18 August, fifteen stable releases shipped — roughly one every two weeks. As I write this, 1.160.0rc1 is already out too. The upgrade notes are not a single "what's new" list either; they contain items that demand action from you — the MSC3861 removal in 1.157.0, Ubuntu 25.10 support being dropped in favour of 26.04 in 1.158.0, the GPG signing key of the Debian/Ubuntu package repository being renewed in 1.159.0.

Note the licensing side too, because it comes up in corporate decision-making: Synapse now lives in the element-hq/synapse repository and is distributed under a dual model of AGPL or a commercial licence. The repository's README also politely reminds you that no support is provided unless you have a subscription from Element. Running your own server means owning the support too.

A Decision Framework

I would walk this list before installing:

  1. Choose server_name permanently. It is embedded in identities and cannot be changed later. Apex domain or subdomain — make that call on day one.
  2. Set up delegation from the start. Either serve_server_wellknown or a correct .well-known response; if you must use SRV, use _matrix-fed._tcp. Never add a path after the port in proxy_pass.
  3. Create Postgres with the right locale. --encoding=UTF8 --locale=C --template=template0. allow_unsafe_locale is not a fix, it is a deferred outage.
  4. Write retention policies on day one. media_retention, and retention if you need it; both delete nothing by default.
  5. Cap your federation appetite. Turn on limit_remote_rooms; keep complexity low on a small server.
  6. Pick your identity direction now. For a new deployment, start directly on MAS; the experimental MSC3861 flag no longer exists.
  7. Back up the signing key. Together with Postgres and media_store, at a consistent moment. The file at signing_key_path is your server's federated identity; it is the one file whose loss cannot be undone.
  8. Build the habit of reading upgrade notes. On a worker deployment this is not a preference but a prerequisite.

Conclusion

Installing Synapse has become easy; keeping it federated still requires operational discipline. You will have noticed what all the line items above share: nearly every one is a default that stays silent on installation day and only speaks up months later. Media is never deleted, history is never pruned, the complexity check is off, .well-known is not served, and the identity layer shifts under your feet.

In the end the choice has less to do with technology than with intent. If your aim is to talk to the outside world, federation is exactly what you want, and you pay for it in disk, DNS and maintenance windows. If your aim is internal communication only, closing the door with federation_domain_whitelist: [] and using Matrix simply as a good protocol is an entirely legitimate choice. The bad third state is leaving federation open because "it may as well stay on", without ever costing it out. Ask your own deployment this question: if this server joins a ten-thousand-member room tomorrow, who pays the bill, and out of which budget?

Official Sources

Top comments (0)