Cobra: Making Transactional Key-Value Stores Verifiably Serializable

Reader-compiled version, generated by Claude Opus 5.5 from the authors' source file for a visual-first ML researcher (postdoc, LLM training and RL) who reads Python, wants to understand how the algorithm works, and knows graphs, GPUs, matrix multiplication and some SAT, but not databases or isolation levels. The authoritative text is the published paper.

added for youHow this version is organized. It starts from the search problem (what is being searched for, and why it is hard), then walks through Cobra's encoding one technique at a time, each led by a diagram. A full worked example traces the paper's Figure 3 on a six-transaction history, followed by Figure 3 translated to Python. The scaling machinery (§4) and the evaluation (§6) come after, at lower depth. Purple boxes and tags like this one mark my additions: they are not the authors' words. Each section names the part of the paper it comes from.

1. The problem, as a search problem

(from Abstract, §1, §2.2, §2.3)
A three-transaction history and its polygraph: one known edge and one binary constraint. Observed history (one key, x) T1W(x = 1) T2W(x = 2) T3R(x) returns 1 Polygraph wr(x): T3 read T1's write T1 T3 T2 constraint: keep (T3,T2) or keep (T2,T1)
Redrawn from the paper's inline polygraph example (§2.3). Solid arrow: a known edge (T3 read the value T1 wrote). The two dashed arrows joined by an arc form one constraint: T2 cannot sit between T1 and T3 (otherwise T3 would have read 2), so either T2 comes after T3 or before T1.
The two ways to resolve the constraint; both are acyclic. Option A: keep (T3, T2) T1 T3 T2 acyclic: order T1, T3, T2 Option B: keep (T2, T1) T2 T1 T3 acyclic: order T2, T1, T3
added for youThe search: pick one side of every constraint; the history passes if some set of picks gives a graph with no cycle. Here both picks work, so the history is serializable. A violation is a history where every combination of picks produces a cycle.

What is being checked. A cloud database runs transactions: groups of reads and writes on keys. It promises serializability: there exists one total order of the committed transactions such that running them one at a time in that order produces exactly the results the clients saw (§2.2). The strict variant additionally requires that order to agree with real time. The paper's question: can the clients check that promise while treating the database as a black box, seeing only the requests they sent and the responses they got back (§1)?

Why it is a search. If the database told you, for each key, the order in which its versions were written (Adya's version order), you would build the dependency graph and test it for a cycle. But the version order lives inside the database and is not exposed. So you must ask whether any version order yields an acyclic dependency graph (§2.2). Checking black-box serializability has long been known to be NP-complete (§1).

added for youBridge from ML. Think of the version order as a latent variable. You observe input/output pairs from a black-box function and ask: does there exist a setting of the latent that explains every observation? Here the "explanation" is a directed acyclic graph over transactions, and each unknown is a binary choice. It is a satisfiability question, not an optimization one: one witness suffices to accept, and rejecting means showing no witness exists.

Why existing exact methods do not scale. Biswas and Enea (BE) lowered the complexity to polynomial time under natural restrictions that hold here, but the number of clients appears in the exponent (e.g., 14 clients means O(n^14)), and BE has no mechanism for a continuous, ever-growing history (§1).

The paper's bet. SAT and SMT solvers routinely "solve" problems whose general form is intractable, so it ought to be possible to verify serializability in many real-world cases (§1). Cobra starts from an SMT solver suited to graph properties (MonoSAT) and adds domain-specific techniques that shrink the search before the solver sees it.

Headline claim. Cobra is the first system that combines (a) black-box checking, of (b) serializability, while (c) scaling to real-world online transactional processing workloads (Abstract, §1, §7).

Small glossary of database terms used below added for you
transaction
a group of reads and writes that is supposed to take effect as a unit; it ends in commit or abort
key-value store
a database whose interface is read(key) and write(key, value); "database" in this paper always means a transactional key-value store (§1)
read-dependency (wr)
Tj reads the value Ti wrote, so Ti → Tj. Visible directly in the history (§2.2)
write-dependency (ww)
Ti writes a key and Tj overwrites it, so Ti → Tj. Needs the version order (§2.2)
anti-dependency (rw)
Ti reads a value that Tj later overwrites, so Ti → Tj. Needs the version order (§2.2)
serialization graph
transactions as nodes, all three dependency kinds as edges. A history is serializable iff some version order makes this graph acyclic (§2.2)
session
a client's sequence of non-overlapping transactions (§2.1)

2. Who checks whom: the setup

(from §2.1, Figure 1)
Cobra's architecture: clients, collectors and verifier in one trust domain; the database outside it. trust domain (dashed) client client collector collector database(untrusted) requests / responses verifier history accept / reject
Redrawn from Figure 1 (simplified to two clients). The verifier is off the critical path but must keep up on average.

Performance requirement. Verifier capacity (transactions/sec) must be at least the database's average offered load over a long interval such as a day, not its peak: being off the critical path, it can catch up.

3. Brute force: the polygraph

(from §2.2–§2.3)

The figure in §1 above is a polygraph, the data structure the brute-force approach is built on. Formally, a polygraph P = (V, E, C) is a directed graph (V, E), the known graph, together with a set C of bipaths: pairs of edges ⟨(v, u), (u, w)⟩ with (w, v) ∈ E. Read it as "either u happened after v, or else u happened before w". For a history:

A graph (V′, E′) is compatible with (V, E, C) if V = V′, E ⊆ E′, and for every ⟨e1, e2⟩ ∈ C exactly one of e1, e2 is in E′. A fact proved in Appendix B of the extended version: there is an acyclic graph compatible with the polygraph of history H iff there is an acyclic serialization graph of H, i.e. iff H is serializable.

Why brute force fails. The search has |C| binary choices, so 2^|C| possibilities, and |C| itself is large: ∑k∈K rk · (wk − 1), where K is the set of keys and rk, wk are the numbers of reads and writes of key k. A sum of quadratic terms.

added for youIn ML terms: 2^|C| is the size of the hypothesis space, and |C| grows with (reads × writes) per key. Everything in §3 of the paper is about shrinking |C| or fixing choices before the solver starts, the same instinct as constraint propagation before branching in a SAT solver.

4. Cobra's pipeline

(from §3 introduction, Figure 2, Figure 3)
The verifier's pipeline within one round. history collectors graph g from round i−1 verifier round i garbage collection create known graph combining writes coalescing constraints pruning (GPU) MonoSAT accept or reject
Redrawn from Figure 2. Within a round: build the known graph, shrink the constraints three ways, then hand the rest to the solver. Across rounds, garbage collection carries the graph g forward (§4).

Cobra keeps the brute-force skeleton (known graph + binary constraints + acyclicity) but changes the constraint format. A constraint is now a pair of sets of edges ⟨A, B⟩: meeting it means including all of A and none of B, or vice versa. A graph (V′, E′) is compatible with known graph G = (V, E) and generalized constraints C if V = V′, E ⊆ E′, and for all ⟨A, B⟩ ∈ C: (A ⊆ E′ ∧ B ∩ E′ = ∅) ∨ (A ∩ E′ = ∅ ∧ B ⊆ E′).

Validity (C10). The authors prove (Appendix B of the extended version) that there exists an acyclic graph compatible with the constraints Cobra constructs on a history if and only if the history is serializable. So the shrinking loses nothing.

The three refinements, each motivated by a common workload pattern: combining writes (read-modify-write transactions, §3.1), coalescing constraints (reads outnumber writes, §3.2), and pruning with all-pairs reachability computed on a GPU (§3.3). Then MonoSAT solves what remains (§3.4).

5. Technique 1: combining writes

(from §3.1; Figure 3 lines 17–22, 27–35, 43–58)
Combining writes: four constraints in the basic polygraph become one ordering question between two chains. Basic polygraph W1W3 wrwr W3 right before W1? T1 T2 T3 T4 R2 W2R4 W4 4 constraints Cobra's view: two chains T1 T2 T3 T4 [W1, W2][W3, W4] 1 constraint: which chain first
Redrawn from the paper's inline figure in §3.1. Four transactions on one key: W1 and W3 are blind writes; T2 = (R2, W2) reads from W1 and T4 = (R4, W4) reads from W3. Left: the basic polygraph has four constraints (only one possibility, "W3 immediately before W1", is drawn, as a dashed black arrow). Right: Cobra infers two chains; only the order between them is uncertain. added for youThe specific edges on the right, (T2, T3) versus (T4, T1), are what Figure 3 lines 65–68 produce here (no transaction reads either chain's tail); I derived them, the paper's figure shows only the shaded chains.

The pattern. A read-modify-write (RMW) transaction reads a key and then writes it: e.g. read stock count, decrement, write back. If T reads key k from T′ and then writes k, then T's write must come immediately after T′'s write in k's version order. Two RMWs cannot both claim to be the immediate successor of the same write; if they do, Cobra rejects on the spot (Figure 3 lines 20–21: "multiple consecutive writes, not serializable").

Chains. A chain is a sequence of transactions whose writes to a key are consecutive. Cobra starts with every write as a one-element chain (line 32), then for each RMW t and the transaction t′ holding the prior write, concatenates t′'s chain with t's chain (lines 22, 44–51). The uncertainty is now only about the order of chains, not individual writes: in the figure, [W1, W2] → [W3, W4] or [W3, W4] → [W1, W2]. The basic polygraph still considers W3 immediately before W1; Cobra recognizes that as impossible.

Inferred anti-dependencies. If a non-RMW transaction t reads from u, and v is u's successor in the chain, then t must precede v; otherwise t would sit downstream of v and should have read v's value (or later). Cobra adds t → v directly as a known edge (InferRWEdges, line 53). In the brute-force encoding the analogous edges appear as the first component of a constraint; here they are certain.

Constraints between chains. For each pair of chains on a key, Cobra builds one constraint ⟨ES1, ES2⟩. ES1 = edges from the readers of chain_i's tail to chain_j's head (lines 71–72); if the tail has no readers, ES1 is the single edge chain_i.tail → chain_j.head (line 67). ES2 is the same in the other direction (line 62). Pointing from the readers of the tail, rather than from the tail itself, is enough because each reader already has a known edge from the tail.

6. Technique 2: coalescing constraints

(from §3.2; Figure 3 lines 37–40, 60–73)
Coalescing: three constraints that all ask "W1 or W2 first" become one constraint with edge sets A and B. Basic polygraph W1 W2 R3 R4 R5 3 constraints, all asking: W1 or W2 first? Coalesced: one constraint ⟨A, B⟩ W1 W2 R3 R4 R5 A = {(R3, W2), (R4, W2)} B = {(R5, W1)}
Redrawn from the paper's inline figure in §3.2. Five single-operation transactions on one key. Left: the basic polygraph's three constraints are ⟨(R3,W2),(W2,W1)⟩, ⟨(R4,W2),(W2,W1)⟩, ⟨(R5,W1),(W1,W2)⟩ (listed here instead of drawn). Right: Cobra's single constraint: include all of A and none of B, or the reverse.

Many real-world workloads have far more reads than writes. All three constraints above ask the same question: did W1 or W2 happen first? So Cobra groups all reads of the same write into one constraint. The full version would be ⟨A′, B′⟩ with A′ = {(W1, W2), (R3, W2), (R4, W2)} and B′ = {(W2, W1), (R5, W1)}. Cobra drops (W1, W2) because the known edge (W1, R3) plus (R3, W2) already implies W1 → R3 → W2; likewise it drops (W2, W1) because of the known edge (W2, R5). Result: ⟨A, B⟩ = ⟨{(R3, W2), (R4, W2)}, {(R5, W1)}⟩.

In Figure 3 this is Coalesce (line 60): for every pair of chains on a key, one constraint, regardless of how many readers each chain's tail has.

7. Technique 3: pruning on the GPU

(from §3.3, §5; Figure 3 lines 75–85)
Pruning: one side of a constraint closes a cycle with a known path, so the other side is forced. Right: reachability by repeated Boolean matrix squaring. Pruning one constraint known path cycle forced W1 W2 R3 constraint ⟨(R3, W2), (W2, W1)⟩ Reachability = Boolean matmul a b c d A + I(A+I)²(A+I)⁴ row = from, column = to orange = newly reachable stops when the matrix stops changing (at most log |V| squarings)
Left: redrawn from the paper's inline figure in §3.3. R3 reads from W2, so there is a known path W2 ⇝ R3. Choosing (R3, W2) would close a cycle, so (W2, W1) is forced. Right added for you: a four-node chain showing what "transitive closure by repeated squaring of the Boolean adjacency matrix" (§5) computes. The upper-triangular shape is what you get when vertices are indexed in topological order, which Cobra exploits (see below).

The logic is almost trivial (the paper's word): compute which transaction can reach which in the known graph; then for each constraint ⟨ES1, ES2⟩, if some edge (tx_i, tx_j) in ES1 points backwards along an existing path (tx_j ⇝ tx_i), ES1 is impossible, so add all of ES2 to the known graph and drop the constraint; symmetrically for ES2 (lines 78–84). The interesting part is the design decision to compute reachability on parallel hardware, which works because the computation is iterated Boolean matrix multiplication.

Pruning can run more than once: adding forced edges creates new paths, which may resolve further constraints. The implementation iterates until nothing more is pruned or a configurable maximum number of iterations is reached (to bound the verifier's work). The authors note that a better implementation would stop when the cost of one more iteration exceeds the solver time it saves (§5).

GPU details (§5)

added for youA NumPy sketch of the same idea, for intuition only. This is not Cobra's code (Cobra's GPU part is 550 lines of CUDA/C++), and it omits the triangular and sparse/dense optimizations.

import numpy as np

def transitive_closure(A):              # A: |V| x |V| 0/1 adjacency matrix
    R = (A + np.eye(len(A), dtype=A.dtype)) > 0
    while True:
        R2 = (R.astype(np.int32) @ R.astype(np.int32)) > 0   # Boolean matmul
        if (R2 == R).all():
            return R                    # R[i, j]: j is reachable from i
        R = R2

If you train models, the structure is familiar: the hot loop is a matmul, so throughput comes from the same libraries you already use.

8. Solving with MonoSAT

(from §3.4)

What remains: find an acyclic graph that contains the known graph and satisfies every remaining constraint. Cobra's encoding:

Why not plain SAT. Encoding graph acyclicity as SAT formulas is expensive (a claim by Janota et al., which the authors also observed, §6.1). MonoSAT is an SMT solver that includes SAT modulo monotonic theories and efficiently encodes and checks graph properties such as acyclicity.

Why not drop the solver. In the limit, Cobra could re-implement the solver. The authors' framing of the division of labor: Cobra's preprocessing exploits structure specific to verifying serializability, while the solver exploits residual structure common to many graph problems, and brings many prior optimizations with it.

added for youBridge from ML-for-theorem-proving. If you have used SAT as a backend, "SMT" means SAT plus a theory solver that reasons about some domain natively. A rough picture: in plain CNF, "this graph is acyclic" has to be spelled out with auxiliary variables and many clauses; with a graph theory, the solver tracks the graph induced by the current partial assignment and can reject a partial assignment as soon as it closes a cycle. The paper itself states only what is in the paragraph above; the internals of MonoSAT are described in its own paper (Bayless et al., AAAI 2015).

9. Worked example, end to end added for you

(traces Figure 3 on a history I constructed; not from the paper)

Six transactions on one key x, plus one session-order fact. The example is mine; every step follows Figure 3's line numbers.

TxOperationsReads fromNote
T1W(x = 1)blind write
T2R(x) → 1, W(x = 2)T1RMW
T3W(x = 3)blind write
T4R(x) → 3T3
T5R(x) → 2T2same session as T3, issued after it
T6R(x) → 1T1
Known graph and the single remaining constraint for the worked example. wr wr rw (line 58) wr wr session ES1 ES2 T3 T4 T1 T6 T2 T5 W(x=3) W(x=1) R(x)→3 R(x)→1 R→1, W(x=2) R(x)→2
Black: read-dependencies (line 14). Grey: session-order edge (line 24). Blue solid: anti-dependency inferred by InferRWEdges (line 58). Dashed: the one constraint from Coalesce, ES1 = {(T4, T1)} versus ES2 = {(T5, T3)}.
  1. CreateKnownGraph (lines 7–25). Read-dependency edges: (T1,T2), (T3,T4), (T2,T5), (T1,T6). readfrom: ⟨x,T1⟩ → {T2, T6}, ⟨x,T3⟩ → {T4}, ⟨x,T2⟩ → {T5}. T2 is an RMW reading from T1, so wwpairs[⟨x,T1⟩] = T2 (line 22); no other RMW claims T1, so no reject. Session order adds (T3, T5) (line 24).
  2. Initial chains (lines 29–32). chains[x] = {[T1], [T2], [T3]}.
  3. CombineWrites (lines 43–51). wwpairs says T2 immediately follows T1: chains[x] = {[T1, T2], [T3]}.
  4. InferRWEdges (lines 53–58). In chain [T1, T2], readers of T1 are {T2, T6}; T2 is the successor itself, so only (T6, T2) is added: T6 read 1, so it must precede the write of 2.
  5. Coalesce (lines 38–40, 60–73). One pair of chains. ES1 = GenChainToChainEdges([T3], [T1,T2]): readers of tail T3 are {T4}, so ES1 = {(T4, T1)}. ES2 = GenChainToChainEdges([T1,T2], [T3]): readers of tail T2 are {T5}, so ES2 = {(T5, T3)}.
  6. Prune (lines 75–85). Is there a path T1 ⇝ T4 (would make ES1 cyclic)? No. Is there a path T3 ⇝ T5 (would make ES2 cyclic)? Yes, the session edge. So ES2 is impossible: add ES1's edge (T4, T1) to the known graph and drop the constraint.
  7. Solve. No constraints remain; MonoSAT only has to confirm the known graph is acyclic. It is: one valid serial order is T3, T4, T1, T6, T2, T5. Accept.

Size of the search, compared. Counting the basic polygraph's constraints directly from the definition in §2.3: read-dependency T1→T2 has 1 other writer (T3), and T1→T6, T3→T4, T2→T5 have 2 each, so |C| = 7 and the brute-force space is 2^7 = 128 (derived). After combining and coalescing there is 1 constraint (2 possibilities, derived: 2^1), and after pruning there are 0. (The paper's general expression ∑ rk(wk − 1) gives 4 × 2 = 8 here; it counts one more because the definition of C excludes Tk = Tj when the reader T2 also writes x.)

Question for you: what if T4 were also in a later session position after T2?

Suppose a session-order edge (T2, T4) existed. Then ES1's edge (T4, T1) would close T1 → T2 → T4 → T1, and ES2's edge (T5, T3) would still close T3 → T5 → T3. Prune checks ES1 first (line 79), finds the cycle, and adds ES2 to the known graph, which now contains a cycle. The known graph is cyclic, so the history is rejected. According to §5, the certificate Cobra reports is then a cycle in the known graph (or, when the solver rejects, a set of unsatisfiable clauses from MonoSAT).

10. Figure 3 in Python

(from Figure 3)

Translation of the paper's pseudocode. Cobra's actual implementation is in Java and CUDA/C++ (Figure 4). Line-for-line with the original; the original line number is the trailing comment. Helpers the pseudocode also leaves abstract (Graph, Reject, add_session_order_edges, transitive_closure, tr.reaches, a transaction's read/write accessors) are assumed. Python mechanics only: reject becomes raise Reject; sets of lists become lists of lists (lists are unhashable); line 78 iterates over a copy because the loop removes from con.

def construct_encoding(history):                                  # 1
    g, readfrom, wwpairs = create_known_graph(history)            # 2
    con = gen_constraints(g, readfrom, wwpairs)                   # 3
    con, g = prune(con, g)          # §3.3, executed one or more times  # 4
    return con, g                                                 # 5
                                                                  # 6
def create_known_graph(history):                                  # 7
    g = Graph()                     # the known graph                   # 8
    wwpairs = {}                    # {(key, tx): tx} consecutive writes # 9
    readfrom = defaultdict(set)     # {(key, tx): {tx}} write -> readers # 10
    for tx in history:                                            # 11
        g.nodes.add(tx)                                           # 12
        for rop in tx.reads:                                      # 13
            g.edges.add((rop.read_from_tx, tx))  # read-dependencies # 14
            readfrom[(rop.key, rop.read_from_tx)].add(tx)         # 15
                                                                  # 16
        # detect RMW (read-modify-write) transactions                # 17
        for key in tx.keys_read & tx.keys_written:                # 18
            rop = tx.read_op(key)                                 # 19
            if wwpairs.get((key, rop.read_from_tx)) is not None:  # 20
                raise Reject  # multiple consecutive writes, not serializable # 21
            wwpairs[(key, rop.read_from_tx)] = tx                 # 22
                                                                  # 23
    add_session_order_edges(g)      # §4.2                              # 24
    return g, readfrom, wwpairs                                   # 25
                                                                  # 26
def gen_constraints(g, readfrom, wwpairs):                        # 27
    # each key maps to set of chains; each chain is an ordered list  # 28
    chains = defaultdict(list)      # {key: [chain, ...]}               # 29
    for tx in g.nodes:                                            # 30
        for wrop in tx.writes:                                    # 31
            chains[wrop.key].append([tx])  # one-element list          # 32
                                                                  # 33
    combine_writes(chains, wwpairs)        # §3.1                     # 34
    infer_rw_edges(chains, readfrom, g)    # infer anti-dependency    # 35
                                                                  # 36
    con = []                                                      # 37
    for key, chainset in chains.items():                          # 38
        for chain_i, chain_j in itertools.combinations(chainset, 2):  # 39
            con.append(coalesce(chain_i, chain_j, key, readfrom))  # §3.2 # 40
                                                                  # 41
    return con                                                    # 42
def combine_writes(chains, wwpairs):                              # 43
    for (key, tx1), tx2 in wwpairs.items():                       # 44
        # By construction of wwpairs, tx1 is the write immediately    # 45
        # preceding tx2 on key. Thus, we can sequence all writes      # 46
        # prior to tx1 before all writes after tx2, as follows:       # 47
        chain1 = next(c for c in chains[key] if c[-1] is tx1)     # 48
        chain2 = next(c for c in chains[key] if c[0] is tx2)      # 49
        chains[key].remove(chain1); chains[key].remove(chain2)    # 50
        chains[key].append(chain1 + chain2)                       # 51
                                                                  # 52
def infer_rw_edges(chains, readfrom, g):                          # 53
    for key, chainset in chains.items():                          # 54
        for chain in chainset:                                    # 55
            for i in range(0, len(chain) - 1):  # i in [0, len-2]    # 56
                for rtx in readfrom[(key, chain[i])]:             # 57
                    if rtx is not chain[i + 1]: g.edges.add((rtx, chain[i + 1]))  # 58
                                                                  # 59
def coalesce(chain1, chain2, key, readfrom):                      # 60
    edge_set1 = gen_chain_to_chain_edges(chain1, chain2, key, readfrom)  # 61
    edge_set2 = gen_chain_to_chain_edges(chain2, chain1, key, readfrom)  # 62
    return (edge_set1, edge_set2)                                 # 63
                                                                  # 64
def gen_chain_to_chain_edges(chain_i, chain_j, key, readfrom):    # 65
    if not readfrom[(key, chain_i[-1])]:       # tail has no readers  # 66
        edge_set = {(chain_i[-1], chain_j[0])}                    # 67
        return edge_set                                           # 68
                                                                  # 69
    edge_set = set()                                              # 70
    for rtx in readfrom[(key, chain_i[-1])]:                      # 71
        edge_set.add((rtx, chain_j[0]))                           # 72
    return edge_set                                               # 73
                                                                  # 74
def prune(con, g):                                                # 75
    # tr is the transitive closure (reachability of every two nodes) of g  # 76
    tr = transitive_closure(g)      # standard algorithm; see [70, Ch.25] # 77
    for (edge_set1, edge_set2) in list(con):                      # 78
        if any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in edge_set1):    # 79
            g.edges |= edge_set2                                  # 80
            con.remove((edge_set1, edge_set2))                    # 81
        elif any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in edge_set2):  # 82
            g.edges |= edge_set1                                  # 83
            con.remove((edge_set1, edge_set2))                    # 84
    return con, g                                                 # 85

After construct_encoding, Cobra feeds the known graph g and constraints con to the solver (§3.4), which searches for a graph that includes g's edges, meets every constraint, and is acyclic.

11. Strict serializability and clock drift

(from §1(b), §3.5)

For the strict variant, Cobra adds real-order edges (capturing the order of transactions that do not overlap in real time) to the known graph, then runs the same algorithm. Timestamps come from the database if it exposes them (e.g. Google Spanner) or from Cobra's collectors. Instead of comparing all pairs (quadratic), Cobra borrows a prior algorithm that materializes the time-precedence partial order in O(n + z) time, where n is the number of transactions and z is the minimum number of real-order edges needed.

Clock drift. Collector clocks may disagree, so Cobra assumes they differ by less than a clock drift threshold (100 ms by default) and adds the threshold to every commit timestamp. Two transactions then get a real-order edge only if one's original commit is earlier than the other's start by at least the threshold; everything within a threshold window counts as concurrent. If clocks actually differ by more than the threshold, Cobra may falsely reject a serializable history.

Why the paper weights the non-strict variant: it is the harder computational problem (real-time edges shrink the search), some databases claim only the non-strict variant or do not specify, and the strict case degenerates toward the non-strict one under heavy concurrency or clock drift, where the techniques of §3.1–§3.3 are needed again.

12. Verifying forever: rounds, fences, garbage collection

(from §1, §4)

Cobra verifies in rounds, for two reasons: new history keeps arriving, and there is a limit to the problem size one solve can handle. Each round reuses the graph g from the previous round (Figure 3, line 5) and adds new nodes and edges. The problem is keeping g bounded: which transactions can be deleted safely?

Why deletion is dangerous (§4.1)

Deleting T2 hides the cycle T4 to T2 to T3 to T4. wr(x) wr(x) wr(y) rw(x) ⇝ (path) T1 T2 T3 T4 W1(x) R2(x) W2(x) W3(y) R4(x) R4(y) delete T2: cycle vanishes
Redrawn from the paper's inline figure in §4.1. T4 arrives later and reads x from T1 and y from T3. Since T2 overwrote T1's x, T4 → T2 (anti-dependency), giving the cycle T4 → T2 ⇝ T3 → T4 (red). If T2 had been deleted, the violation would go undetected. No malice is needed: a geo-replicated database can serve a stale version from a local replica.

Epochs and fence transactions (§4.2)

Three sessions with fence transactions chained in order on the EPOCH key. session 1 session 2 session 3 1 2 3 4 5 6 7
added for youSchematic, not a paper figure. Dots: normal transactions in session order. Squares: fence transactions, each a read-modify-write of the dedicated key "EPOCH", so they form one chain (blue); a fence's position in that chain is its epoch number. In this picture session 2 has reached epoch 5 and the others have passed it, so epoch_agree = 5 under the paper's definition.

Guarantee. For any transaction Ti with epoch ≤ (epoch_agree − 2) and any transaction Tj, including future ones, with epoch ≥ epoch_agree, the known graph contains a path Ti ⇝ Tj. Proof sketch from the paper: Ti ⇝ (the next fence in Ti's session, epoch ≤ epoch_agree − 1) ⇝ (the fence with epoch epoch_agree) ⇝ Tj via session order.

Garbage collection (§4.3)

Cobra deletes a transaction T only if (i) T is superseded: no future transaction can precede T or directly succeed T in the known graph; and (ii) T is not on any potential cycle through constraint edges whose resolution future transactions could affect.

13. Implementation

(from §5, Figure 4)
Cobra componentLOC written/changed
Client library: history recording620 lines of Java
Client library: database adapters900 lines of Java
Verifier: data structures and algorithms2k lines of Java
Verifier: GPU optimizations550 lines of CUDA/C++
Verifier: history parser and others1.2k lines of Java

14. Evaluation

(from §6)

Three questions: the verifier's costs and limits versus baselines; its sustainable round-to-round capacity; and its overhead on clients, storage and network.

Setup

One-shot verification (§6.1)

Figure 5 (plot, values not reproduced). Verification time (s, 0–14, lower is better) against number of transactions (0–10k) on BlindW-RW, for MiniSAT-BE, nonSAT, Z3-arith, MonoSAT-polygraph and Cobra. Caption: Cobra's running time is shorter than the baselines' on BlindW-RW, the same holds on the other benchmarks, and verification runtime grows superlinearly. Text: on all five benchmarks, Cobra does better than MonoSAT-polygraph and Z3-arith, which do better than MiniSAT-BE and nonSAT. 24 clients. See the published PDF for the curves.

C2, C3: what "10×" measures. Cobra's core (single-round) verification improves on baselines by 10× in the problem size it can handle for a given time budget, i.e. in verification cost. This is not a throughput figure. Example: Cobra finishes checking 10k transactions in 14 seconds, whereas baselines can handle only 1k or less in the same time budget (§1, §6.4).

C5: real bugs. Cobra detects all five serializability violations collected from real systems' bug reports, in reasonable time:

ViolationDatabase#TxnsTime to detect
G2-anomalyYugaByteDB 1.3.1.037.2k66.3s
Disappearing writesYugaByteDB 1.1.10.02.8k5.0s
G2-anomalyCockroachDB-beta 201608294461.0s
Read uncommittedCockroachDB 2.120*1.0s
Read skewFaunaDB 2.5.48.2k11.4s

From Figure 6. * The bug report contains only a small fragment of the history. #Txns is the size of the violation history; Time is Cobra's runtime to detect it.

Figure 7 (stacked bars, values not reproduced). Runtime split into constructing / pruning / solving (seconds) on 10k-transaction workloads: TPC-C, C-Twitter, C-RUBiS, BlindW-RM, BlindW-RW, BlindW-WM. Caption: pruning dominates for read-mostly workloads; solving dominates for workloads with many writes. Text: TPC-C (RMW only) has no constraints, so no pruning; for workloads with many reads and RMWs, pruning dominates because Cobra's own logic identifies concrete dependencies; with many blind writes, solving grows because fewer constraints are eliminated. The authors note a write majority is not typical of OLTP workloads, where reads dominate.
Figure 8 (log-scale bars, values not reproduced). Differential analysis on TPC-C, C-Twitter and BlindW-RW at 10k transactions: Cobra; Cobra without pruning; Cobra without pruning and coalescing (= MonoSAT plus write combining); MonoSAT baseline. Timeout 10 min. Caption: on TPC-C, combining writes solves all the constraints; on C-Twitter each component contributes meaningfully; on BlindW-RW pruning is essential, because blind writes cannot benefit from the other two techniques.

C6: strict serializability under clock drift. Eight clients run BlindW-RW on 1k keys for one second at 2k transaction/sec, issuing 20 transactions every 10ms; 2,000 transactions in total; clock drift threshold up to 100 ms. Cobra outperforms the baselines (MonoSAT-polygraph and Z3-arith) by 45× and 107× in verification time.

Figure 9 (plot, values not reproduced). Verification time (s) against clock drift threshold (0–100 ms) for Z3-arith, MonoSAT-polygraph and Cobra.
Question for you: which workload should be hardest for Cobra, and why?

Blind writes. They do not form RMW chains, so combining writes does not help; they leave more constraints for pruning and the solver. The paper's results agree: solving dominates on the BlindW write-heavy variants (Figure 7), and BlindW-RW and BlindW-WM eventually exceed GPU memory in the scaling experiment (below).

Scaling (§6.2)

Verification capacity for a workload = max over round size #tx_r of (#tx_r / t_r), where t_r is the average time for one round. Setup: RocksDB, 24 concurrent clients, a fence every 20 transactions, a 100k-transaction history generated ahead of time.

C7. Best capacity is 2.3k txn/sec for BlindW-RM (at #tx_r = 5k) and 1.2k txn/sec for C-RUBiS (at #tx_r = 2.5k). C-Twitter and TPC-C are similar (not depicted). Too-small rounds leave too little to garbage-collect, so work from prior rounds is re-analyzed; too-large rounds hit the superlinear growth of verification time.

Figure 10 (plot, values not reproduced). Verification throughput (txn/sec) against transactions per round (1k–10k) for BlindW-RM (dashed) and C-RUBiS (solid).

Out of memory. On BlindW-RW and BlindW-WM, history eventually exceeds GPU memory: blind writes cannot benefit from combining writes, so many constraints remain, transactions stay involved in uncertain constraints, and they cannot be collected.

Fence frequency trade-off. More frequent fences give smaller epochs, earlier garbage collection, smaller and easier per-round problems (fences add ordering), so higher verifier throughput; but they cost peak client throughput. If client load is constant, choose the frequency where verifier throughput equals client offered load.

Figure 11 (plot, values not reproduced). Client and verifier throughput against number of transactions between fences per client (10–60), BlindW-RM, round size fixed at 5k. Client throughput is normalized to the workload without fences. Normal transactions have 8 operations; fences have 1–2.

Online overheads (§6.3)

Baseline: the legacy system (unmodified database library, no history recording). Up to 256 clients to saturate each database.

C8 (C-Twitter).

Figure 12 (plots, values not reproduced). Latency against throughput, Cobra versus the original setup, in three panels: (a) RocksDB, (b) PostgreSQL, (c) Google Datastore.

C9: network and storage per 1k transactions (Figure 13). Overhead comes from fence transactions and the metadata (transaction ids and write ids) added by the client library. Network overhead is 2.17%–7.28% of traffic across the four workloads.

WorkloadNetwork trafficNetwork percentageHistory size
BWrite-RW (label as printed)227.4 KB7.28%245.5 KB
C-Twitter292.9 KB4.46%200.7 KB
C-RUBiS107.5 KB4.53%148.9 KB
TPC-C78.2 KB2.17%1380.8 KB

Summary (§6.4)

C4. Sustainable verification throughput of 2k txn/sec on the workloads tested ("2000 transactions/sec, equivalent to 170M/day"). The requirement is to match the database's average load (§2.1). For comparison the paper cites Apple Pay at 33M txn/day and Visa at 150M txn/day, noting that a payment "transaction" may be several database transactions, so the comparison is inexact.

15. Related work and discussion

(from §7, §8)

Who would use it (§8). The §2.1 scenarios, or as the checker in a testing framework such as Jepsen, injecting faults into OS, storage or network without instrumenting the database. The verifier must match the database only in long-term average transactions/sec; it does different work per transaction (the database handles geo-replication, concurrency control, durability and more; the verifier is a single machine and purely algorithmic). Given a certificate, a user could supply a candidate serialization order to enable roll back and replay.

Future work named by the authors. Fault tolerance for verifier and collectors (even partial histories give meaningful results: a cycle in a partial history is a violation of the full history); other isolation levels; native range queries and operators like sum and join; more aggressive garbage collection, e.g. letting the verifier query the database to resolve constraints; and whether real-world workloads ever trigger the exponential worst case. Their intuition: low contention per key yields few constraints, and high contention with enough reads imposes more ordering.

Limitations

(from Part C L1–L8: §1, §2.1, §3.5, §6.1, §6.2, §8)
  1. L1. No termination guarantee. Nothing guarantees Cobra finishes in reasonable time; the worst case is in principle exponential (the problem is NP-complete). The experiments on real workloads do terminate.
  2. L2. Key-value API only. No range queries or SQL operations such as join and sum, unless translated to key-value operations.
  3. L3. Client model. Clients may be multithreaded but not async/event-driven.
  4. L4. No crashes assumed. The verifier and collectors are assumed not to crash; their fault tolerance is future work.
  5. L5. No violations found in the wild. The authors did not identify serializability violations in the wild. In their words: "we lack a sensational headline."
  6. L6. Blind-write-heavy workloads. Cobra can be slow with many unconstrained (blind) writes (BlindW-WM). On BlindW-RW and BlindW-WM, history eventually exceeds GPU memory in the scaling experiment.
  7. L7. Detection only; clock assumption. Cobra detects violations but cannot prevent them. For strict serializability it assumes collector clocks differ by less than a threshold (100 ms by default); beyond that it may falsely reject a serializable history.
  8. L8. Trust and visibility. Clients, collectors and verifier must sit in one trust domain, and the verifier needs the full history of requests and responses.

After publication: not part of the paper

Snapshot dated 2026-09-30, compiled by the blog's editor (an AI assistant working for the first author) and checked against each paper's own page. It is not peer-reviewed and was not written by the paper's authors. I did not browse the web when compiling this version, so it is reproduced as given and may be out of date.

Who cites this paper

Work that builds on it

Black-box checking of transactional isolation became an active line of work after 2020. The entries below cite Cobra and build on it directly: they reuse its techniques, extend it, or use it as the baseline. Each relation is stated as that paper reports it.

WorkVenueRelation to Cobra (as reported by that work)
PolySI (Huang, Liu, Chen, Wei, Basin, Li, Pan)PVLDB 2023Carries the polygraph-plus-MonoSAT approach from serializability to snapshot isolation; compares against a Cobra-based SI checker.
Viper (Zhang, Ji, Mu, Tan)EuroSys 2023Snapshot-isolation checker that integrates Cobra's combining-writes and coalescing-constraints optimizations.
Plume (Liu, Gu, Wei, Basin)OOPSLA 2024Sound and complete checking of weak isolation levels; compares against Cobra.
Emme / King Cobra (Clark, Rigger, Wickerson, Donaldson)EuroSys 2024; TOCS 2026Recovers version certificates from the database; the TOCS extension builds "King Cobra," a modified Cobra that can use version order.
AWDIT (Møldrup, Pavlogiannis)PLDI 2025Optimal-complexity weak-isolation tester; generates evaluation histories with Cobra's benchmarks.
Chronos / Aion (Li, Wei, Ouyang, Chen, Yang, Zhang, Pan)ICDE 2025Online checking from database timestamps; does not need Cobra's fence transactions.
MTC (Wei, Xiao, Yang, Liu, Yin, Chen, Pan)ICDE 2025Restricts workloads to mini-transactions; reports large end-to-end gains over Cobra.
Vbox (Sun, Zou)arXiv 2025"Developed from Cobra"; adds predicate reads and writes; reports 60–100× over Cobra on 10K-transaction histories.
VeriStrong (Cai, Liu, Wei, Chen, Pan)PVLDB 2026Drops the unique-value assumption; reports up to 4.4× over GPU-accelerated Cobra.
Boomslang (Tan, Zhang, Mu)arXiv 2026A framework that generalizes Cobra; reimplements Cobra as one module.

State of the art, in one paragraph

Defining isolation levels is largely settled; checking them efficiently is the active problem. Checking serializability and snapshot isolation on a black box is NP-complete in general. Since Cobra, the field has escaped that hardness by three routes: assuming more about the system (timestamps or version certificates from the database); restricting the workload (e.g. mini-transactions); and improving the solver and the encoding. Reported speedups over the previous generation are now one to three orders of magnitude. The weak isolation levels have sound and complete checkers. The open edge is real SQL: with predicates, even cheap levels become hard, and a black-box checker for a genuine SQL workload does not quite exist yet. A maintained comparison of checkers is at naizhengtan.github.io/pages/isolation-checker-matrix.html.

Faithfulness note

Ledger items included

Ledger items omitted

Additions (mine, not the authors')