Skip to main content
Swytch Documentation
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

What is Light Cone Consistency?

There are a lot of consistency models. Linearizable. Sequential. Causal. Eventual. Read-your-writes. Monotonic reads. PRAM. Release. Snapshot isolation. Every paper invents a new one, every database picks its favorite, every developer stops reading around the third definition.

They’re all the same shape.

Light Cone Consistency (LCC) is a framework that says every message-passing system (a CPU compare-and-swap, a Raft cluster, email, a letter dropped in a mailbox) is a causal DAG of messages, observed by a set of nodes, each of which sees only a growing sub-DAG of it. Every consistency guarantee is a constraint on the shape of that sub-DAG.

F(C, O, R) => result

Four components. Any message-passing system you can name is a particular setting of them.


The observer and their sub-DAG

Before the parameters, the mental model.

A message-passing system is a causal DAG: a directed graph where each vertex is a message (a write, a packet, an operation, a letter, a signal) and each edge is a causal dependency, meaning the creator of the later message had already seen the earlier one.

At any moment, each observer sees a sub-DAG of that graph: the subset of messages that have arrived at them, with the edges among them. The sub-DAG grows monotonically. Once you’ve seen a message, you never un-see it. Different observers see different sub-DAGs at different moments, and that’s where everything interesting happens.

Consistency, in LCC, is what we require of each observer’s sub-DAG. C, O, and R describe what must be in it and how it’s shaped. F describes what value you report from it.


The three visibility parameters

The name comes from physics. A light cone is the region of spacetime reachable from an event at the speed of light; everything that could have possibly influenced it, and everything it could possibly influence. LCC takes the metaphor seriously: any operation sits in a cone of things that must already be visible before it can appear, and a cone of observers that need to see it afterward.

C — causal closure. What must already be in your sub-DAG before a given message is allowed to appear in it? If you see a message, which of its ancestors must also be visible? C(none) imposes no closure; messages can appear with missing ancestors. C(explicit) demands full closure; you never see a message without its complete causal history. Everything in between is scoped (per-key, per-session).

O — fork resolution. When the DAG branches (two concurrent messages, neither causally prior to the other), how is the ambiguity resolved into a deterministic sequence? O(trivial) leaves forks alone; observers are allowed to disagree. O(π_key) requires forks resolved per key. O(π_all) requires a global total order everyone agrees on. The mechanism (leader, quorum, cache coherence, CRDT merge) is an implementation detail. O specifies the scope the agreement has to cover.

R — timeliness. How quickly does a message enter your visible sub-DAG after its creation? R(absent) imposes no guarantee; messages may never arrive. R(∞) guarantees eventual delivery. R(δ) bounds delivery by δ. R(0) is instantaneous. R isn’t a performance concern; it’s a correctness parameter. If your system can’t meet R, it’s not operating at the consistency level you claimed.

Set those three dials, and you’ve described what must be visible, in what shape, and by when.


Fork resolution in practice

O is labeled “fork resolution” because that’s what it actually specifies. The work of fork resolution isn’t isolated to O, though; it pulls on all three parameters.

“Concurrent” here isn’t a wall-clock word. Two events are concurrent in the DAG when neither knew about the other, neither could have caused the other. Physics calls this space-like separated: the events sit outside each other’s light cones. In a distributed system, it’s just “two writes arrived, and neither one knew about the other.”

When that happens, something has to pick. And the picking pulls on all three parameters:

  • It’s why you need C, sometimes. You can’t resolve a fork without seeing enough history to recognize it as a fork. When forks are rare and local, a shallow cone is fine. When they span regions or reach back across transactions, C has to go deeper, and deeper closure costs on the wire.
  • It’s what O specifies. The scope O(π) sets is the scope over which the resolution must agree. π_key means per-key agreement. π_all means global agreement. The choice of mechanism is downstream.
  • It bounds R. How long you’re willing to wait for the resolution is how long the operation takes. Cache coherence resolves forks in nanoseconds. Multi-region quorum in hundreds of milliseconds. Partition heal can take hours.

A few of the shapes fork resolution takes in practice:

  • A leader. Raft, Paxos, single-master Postgres; one node decides, everyone else defers.
  • A quorum or timestamp. Spanner’s TrueTime, Cassandra’s last-write-wins, Dynamo’s vector clocks; the system agrees on a tie-breaking rule and every node applies it.
  • Cache coherence. Two CAS attempts hit the same memory word. The coherence protocol picks a winner. Fork resolution in silicon.
  • An algebraic merge. CRDTs don’t pick a winner; they define an operation where both writes combine into a deterministic result. Fork resolution by math, not by vote.

Different mechanisms, different (C, O, R) profiles, same job: when the DAG branches, pick which branch the future depends on.


F: the return-value function

C, O, and R describe what’s in your sub-DAG and what shape it has. F is separate: given the sub-DAG, what value do you actually report?

That sounds like a minor detail. It isn’t. Consider “eventual consistency” and “strong eventual consistency.” Same C, O, R: (none, trivial, ∞). What’s different is F.

Eventual consistency says: F returns some value from the sub-DAG. Maybe the most recent. Maybe a concurrent one. The spec doesn’t pin it down.

Strong eventual consistency says: F is a deterministic function over the sub-DAG (a CRDT merge, for instance). Every observer with the same sub-DAG returns the exact same value.

Same visibility. Different reporting discipline. Different guarantees.

F is orthogonal to the impossibility surfaces (CAP, FLP, and the newer AFC) that constrain C, O, and R. Changing F doesn’t move you across them. The cost of consistency is entirely in what you have to see. F is what you do with what you’ve seen.


The scope claim

This is the part that sounds too big to be true, so let’s test it:

  • CPU compare-and-swap. C: the current value at the address. O: strict linearizable. R: single-digit nanoseconds. Tiny cone, tight knobs.
  • Raft. C: the leader’s committed log. O: linearizable with respect to the leader. R: milliseconds. Larger cone, looser latency.
  • Email. C: the messages you replied to, usually. O: best-effort causal, often broken by threading bugs. R: minutes to hours. Bigger cone, much looser.
  • Snail mail. C: whatever the sender remembered to reference. O: essentially none. R: days. Enormous cone, wide-open knobs.

All four are the same function. Different settings, same function. The distinction between “distributed database” and " postal service" stops being categorical and starts being parametric.

There’s a corollary worth calling out. A CAS and a Raft commit aren’t just two points on the same graph. A CAS is what fork resolution looks like when you shrink R to nanoseconds and bound the cone to a single memory address. Two concurrent CAS attempts with the same expected value are a fork; the cache coherence protocol picks a winner; everything downstream depends on the winner and not the loser. It’s the same shape as a distributed commit, just with a tighter budget and a hardware implementation. Cache coherence isn’t a shortcut around consensus.


Transactions as envelope messages

A transaction, in LCC, isn’t a primitive. It’s an envelope message: a single vertex in the DAG whose causal edges point to every operation it contains. Seeing the envelope means seeing all its constituent operations. Committing the envelope is atomic; the vertex enters the DAG all at once, or not at all.

The analogy: you can have a coherent thought before you speak it aloud.

While you’re thinking, the thought is private. You can revise it, reorder it, throw parts away. None of that is visible to anyone else. When you finally speak, what comes out is a single committed utterance. The envelope enters the public DAG, and from the outside it looks atomic.

Swytch uses exactly this construction for serializable transactions. You build up the operations; Swytch holds them in an envelope; when you commit, the envelope enters the global causal DAG as one vertex with edges to every operation inside. When two envelopes conflict, every node detects the conflict and orders them deterministically from the DAG. Same DAG at every node, same rules, same result. Exactly one envelope commits; the other fails cleanly. No voting, no communication between nodes to decide.


Why this matters for Swytch

LCC is the reason the Swytch engine can give you any achievable consistency level without being rewritten. Internally, every operation is already an F(C, O, R) call over a sub-DAG. Serializable transactions are one setting. Causal consistency on reads is another. The engine doesn’t switch between implementations; it turns the same knobs.

The defaults are serializable consistency for transactions and causal consistency for everything else.


ℹ️ Want the nitty-gritty?

The full LCC paper (the formal definitions, the triangle, the lemmas, the Burckhardt completeness mapping) is on arXiv: [link pending].