Case StudiesProductsBlogOpen SourceBook a call

OpenMLS Looked Like the Perfect Answer, Until It Wasn't

On paper, it checked all the boxes. In practice, I ran into a hard constraint that made it the wrong fit for what I was trying to build.

For a long time, OpenMLS felt like the obvious solution for encrypting shared group state.

It's well-designed, thoughtfully specified, and built to solve a real problem: secure group messaging with strong guarantees around membership, forward secrecy, and compromise recovery.

On paper, it checked all the boxes.

In practice, I ran into a hard constraint that made it the wrong fit for what I was trying to build.

Why OpenMLS was so appealing

MLS is great at what it's designed for: ordered group communication.

Everyone agrees on:

Keys evolve as the group changes. Membership changes advance the epoch. Everything stays tightly synchronized.

For chat, this is exactly what you want.

For shared state, it turns out to be a problem.

The sequential processing wall

The realization came slowly, then all at once:

MLS requires commits to be processed in order.

This is worth being precise about, because MLS has three kinds of message and they behave differently. Application messages are the ones carrying your payload, and they tolerate some reordering: each sender has a ratchet within the epoch, and OpenMLS keeps skipped keys, so a message that arrives late still decrypts. Proposals are unordered by design. Commits are the problem. Each one derives epoch N+1 from epoch N, which means they form a chain you have to walk.

If a client misses a commit or goes offline while the group changes, it can't read anything from the later epochs. It has to catch up, one commit at a time, to rebuild the correct group state.

That's not a flaw, it's a core design property.

But it means:

And that's exactly what distributed state systems need to do.

Where it breaks for shared state

When you're encrypting group messages, an agreed sequence is the point.

When you're encrypting group state, it becomes a liability.

Local-first and offline-first systems assume:

The payload layer would actually be fine here. The trouble is underneath it: if two peers each commit while partitioned, they land on two different epoch N+1s, and there's no merge. One of them has to be thrown away and replayed. A CRDT's whole premise is that this never has to happen.

That's the moment it became clear: MLS and CRDT-style state sync are philosophically incompatible.

The hard-earned lesson

I didn't arrive at this conclusion casually. It took real implementation work to hit this wall.

OpenMLS is not "bad." It's just extremely good at a different problem than the one I needed solved.

MLS secures conversations. State sync secures convergence.

Those are related, but not interchangeable.

What I took away from it

The big lesson wasn't about OpenMLS specifically. It was about respecting design intent.

A system that enforces strict ordering gives you powerful guarantees, but only if your problem actually wants those guarantees.

Once you need:

you need a different abstraction.

It's a humbling reminder that sometimes the hardest part of system design isn't building the thing, it's recognizing when the thing you're excited about doesn't fit.

And moving on anyway.

This remind you of a problem you've been dealing with?

I'd be happy to discuss it with you and see if it's something we could turn into a solution.

Book a call

Newsletter

Subscribe to my newsletter to be the first to hear about what I do next