Cobra: Making Transactional Key-Value Stores Verifiably Serializable

Cheng Tan, Changgeng Zhao, Shuai Mu, and Michael Walfish
NYU Department of Computer Science, Courant Institute · Stony Brook University (Shuai Mu)
OSDI 2020 · published paper · extended version, arXiv:1912.09018

Reader-compiled version, generated by Claude Opus 5.5 from the authors' source file for an 11th-grade student who knows basic Python, likes pictures and worked examples first, and wants the gist. The authoritative text is the published paper.

How to read this page: every section starts with a picture, then a worked example, then a short explanation. Boxes labeled [added for you] are explanations, analogies, or examples written by the AI for you. They are not the authors' words. Everything else is a plain-language retelling of what the paper says.

1. The problem: can you trust a database you cannot see inside?

(from Abstract, §1, §2.1)

trust domain (your side) client client client historycollectorsrecord everything verifier → accept / reject databaseuntrustedblack box requests / responses
Redrawn from the paper's Figure 1 [added for you: redrawn diagram]. Clients, collectors, and the verifier are on your side (dashed box). The database is outside it and is not trusted. The verifier works in the background, off the critical path, but must keep up on average.

A Python way to picture a "key-value store." Think of it as a giant Python dictionary that lives on someone else's computer:

db = {"apples_in_stock": 5, "alice_balance": 100, "bob_balance": 20}

A transaction is a small group of reads and writes that are supposed to happen together, as one unit. Example: move $10 from Alice to Bob.

# one transaction
a = db["alice_balance"]          # read
b = db["bob_balance"]            # read
db["alice_balance"] = a - 10     # write
db["bob_balance"]   = b + 10     # write

Thousands of programs send transactions like this at the same time. The database runs them in parallel to be fast, but it promises the result will look as if they ran one at a time, in some order. That promise is called serializability.

Many modern cloud databases (the paper names Amazon DynamoDB and Aurora, Azure CosmosDB, CockroachDB, YugaByte DB and others) promise serializability. The paper calls it the "gold-standard" correctness property: lots of application code would be wrong if the database gave a weaker guarantee.

But these databases are black boxes. They are run by someone else, on someone else's machines. Bugs, misconfiguration, or operator mistakes inside them could break the promise, and real production systems have broken it. So the paper asks: can the customers check, from the outside only, that the database really kept its promise?

Cobra is the authors' answer. It records every request sent to the database and every answer that comes back (the history), and a separate verifier checks whether that history could have come from some one-at-a-time order. The paper says Cobra is the first system that combines (a) black-box checking, of (b) serializability, while (c) scaling to real-world online transactional processing workloads.

2. What "serializable" means, with a worked example

(from §2.2–§2.3)

T1 writes x=1, y=1 T2 reads x→1, y→0 T1 before T2 (T2 saw new x) T2 before T1 (T2 saw old y)
[added for you: example and diagram] A cycle means "T1 must come before T2 and T2 must come before T1." No one-at-a-time order fits, so this history is not serializable.

Worked example. Start with db = {"x": 0, "y": 0}. Transaction T1 sets both x and y to 1. Transaction T2 reads both and gets x == 1 but y == 0.

Both cannot be true. It is like seeing money leave Alice's account but never arrive in Bob's: a half-finished transaction leaked out. In the paper's language, the "who must come before whom" arrows form a cycle, and a history is serializable exactly when the arrows can be chosen without any cycle.

Here is the hard part. From outside, you often do not know some of the arrows. The paper's own small example has three transactions on one key x:

T1W1(x=1) T3R3(x) reads 1 T2W2(x=2) known: wr(x) option A: T3 → T2 option B: T2 → T1
Redrawn from the paper's example polygraph in §2.3 [added for you: redrawn diagram]. Solid arrow: a known edge. The two dashed arrows joined by an arc form one constraint: exactly one of them must be true.

T3 read the value 1, so T1 came before T3 (a solid, known arrow). T2 also wrote x. T2 cannot sit between T1 and T3, or T3 would have read 2. So either T2 came after T3, or T2 came before T1. Which one is true is unknown.

The paper calls this structure a polygraph: the arrows you know form the known graph, and each "one of these two" choice is a constraint. The history is serializable if and only if you can pick one option from every constraint so that the whole graph has no cycle. (This "if and only if" is proved in the extended version, Appendix B.)

Why that is hard. Each constraint is a yes/no choice, so n constraints give 2n combinations to consider (the paper writes this as 2|C|). For illustration only: 10 constraints give 210 = 1,024 combinations; 100 give more than 1030. A real history has a huge number of constraints, because every read of a key creates a choice for every other write to that key. Computer scientists call this kind of problem NP-complete: no one knows a way to solve every case quickly.

3. Cobra's core idea: shrink the puzzle, then hand it to a solver

(from §1, §3, §3.1–§3.4, Figure 2)

history fromcollectors create known graph 1. combiningwrites 2. coalescingconstraints 3. pruning(on a GPU) 4. MonoSATsolver accept or reject
Redrawn from the paper's Figure 2 (one verifier round) [added for you: redrawn diagram, laid out as a snake to fit a phone].

The authors' starting intuition: although the problem is hard in general, modern solvers (programs that search for an assignment satisfying many logical conditions) often handle real-world cases well. Cobra uses one called MonoSAT, which is good at graph questions such as "is there a cycle?" On its own, though, MonoSAT is still too slow on the brute-force puzzle. So Cobra first shrinks the puzzle with three tricks that use patterns common in real programs:

Trick 1: combining writes (§3.1)

Many transactions read a key and then write the same key (a read-modify-write). The paper's example is a shop:

n = db["apples_in_stock"]       # read
db["apples_in_stock"] = n - 1   # write back

If this transaction read the value written by transaction A, then its own write must come right after A's write. That glues the two writes together.

Cobra glues such writes into chains: ordered lists of writes to one key that must be consecutive. Then it only has to decide the order of whole chains, not of every single write. With two chains [W1, W2] and [W3, W4], only two orders remain: [W1, W2] before [W3, W4], or the reverse.

Trick 2: coalescing constraints (§3.2)

In most real workloads there are far more reads than writes. Many constraints end up asking the same underlying question, such as "did write W1 or write W2 happen first?" Cobra merges all those constraints into one bigger either/or choice between two groups of arrows, so the solver makes one decision instead of many.

Trick 3: pruning (§3.3)

W1 W2 R3 known path W2 ⇝ R3 option R3 → W2: makes a cycle, ruled out so W2 → W1 must hold
Redrawn from the paper's pruning example in §3.3 [added for you: redrawn diagram]. The constraint is ⟨(R3, W2), (W2, W1)⟩.

If the known arrows already give a path from W2 to R3, then choosing "R3 before W2" would close a loop. That option is impossible, so Cobra settles the constraint without the solver and adds the other option to the known graph. To do this quickly, Cobra needs to know, for every pair of transactions, whether one can reach the other along known arrows. That computation can be done as repeated matrix multiplication, which is exactly what graphics cards (GPUs) are good at, so Cobra runs it on a GPU.

Step 4: solving (§3.4)

Whatever is left goes to MonoSAT, which searches for one choice per constraint that leaves no cycle. If it finds one, the history is accepted. If not (or if Cobra already found a cycle among the known arrows), the history is rejected, and Cobra outputs the problematic transactions as evidence (§5).

The authors prove that this shrunken puzzle is still faithful: an acyclic graph compatible with Cobra's constraints exists if and only if the history is serializable (§3; proof in Appendix B of the extended version).

A question to think about

Why do the tricks work well on "real" workloads but might not on others? (Hint: what happens to trick 1 if almost no transaction reads a key before writing it? Section 6 and the limitations box show what the authors found.)

4. Checking forever: rounds, epochs, and fence transactions

(from §1, §4)

old epochs: can be cleaned up time → session 1 session 2 fence fence
[added for you: simplified illustration] Dots are ordinary transactions; blue squares are fence transactions. Fences split history into epochs. The real rule for what can be deleted is more careful than "everything in the gray box"; see the text.

A real database runs all day, so the history never stops growing. Cobra checks it in rounds, each round handling a new piece of history. The catch: in a serializable database, a transaction today could legally read a value written long ago, so the verifier cannot simply forget old transactions; doing so could hide a cycle.

Cobra's fix: every client periodically (for example, every 20 transactions) runs a tiny fence transaction that reads and writes a special key named "EPOCH". Fences divide history into epochs, and they let the verifier prove that sufficiently old transactions are always "before" all future ones. The verifier tracks the frontier (the latest writes that future transactions could still read) and marks older transactions as superseded. Even a superseded transaction is deleted only if it cannot be part of a cycle that future transactions might complete. This garbage collection keeps each round's problem small.

5. What the authors showed

(from §1, §5, §6, Figures 6, 9, 10, 12, 13)

10×
improvement over baselines in verification cost: the size of problem Cobra's core (single-round) check can handle in a given time budget. This is not a throughput number.
10k in 14 s
Cobra finishes checking 10k transactions in 14 seconds; baselines handle only 1k or less in the same time budget.
2k txn/sec
sustainable verification throughput on the workloads tested ("2000 transactions/sec, equivalent to 170M/day").
5 of 5
serializability violations from real systems' bug reports that Cobra detected.

Catching real bugs (Figure 6)

The authors downloaded histories from public bug reports of three real databases, each known to contain a serializability violation, and fed them to Cobra. It found all five.

time for Cobra to detect the violation (seconds) YugaByteDB 1.3.1.037.2k txns 66.3s YugaByteDB 1.1.10.02.8k txns 5.0s CockroachDB-beta 20160829446 txns 1.0s CockroachDB 2.120* txns 1.0s FaunaDB 2.5.48.2k txns 11.4s
Drawn from the table in the paper's Figure 6 [added for you: the paper gives a table; this bar chart shows the same values]. * That bug report contained only a small fragment of the history.
ViolationDatabase#TxnsTime
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

Speed compared with other checkers (Figure 5, Figure 9)

The paper's Figure 5 plots verification time (seconds, 0–14, lower is better) against number of transactions (0–10k) for Cobra and four baseline checkers on one benchmark. Cobra's line is lowest, and time grows faster than linearly as the history grows. The exact plotted values are not reproduced here; see the published PDF.

For strict serializability (a stricter version where the order must also respect real clock time), on 2,000 transactions with a clock drift threshold of 100 ms, Cobra beat the baselines by 45× and 107× in verification time.

Keeping up over time (Figure 10)

Running round after round, the best verification capacity was 2.3k txn/sec for one test workload (BlindW-RM, at 5k transactions per round) and 1.2k txn/sec for an eBay-like auction workload (C-RUBiS, at 2.5k per round). The paper's headline 2k txn/sec corresponds to 170M per day. For comparison the paper cites Apple Pay at 33M txn/day and Visa at 150M txn/day, while noting that a "transaction" there means something slightly different, so the comparison is inexact. The verifier only needs to match the database's average load, not its peak, because it works in the background and can catch up.

Cost to the people using the database (Figures 12, 13)

Optional: the algorithm in Python (Figure 3), and how big Cobra's code is

Translation of the paper's pseudocode. Cobra's actual implementation is in Java and CUDA/C++ (Figure 4). [added for you: translated code]

Each line ends with the original line number. Helpers such as Graph, transitive_closure, all_pairs, reject, and add_session_order_edges are left undefined, just as in the paper. Two Python-only details: the paper's "Set" of constraints is a Python list here (a pair of sets cannot go inside a Python set), and line 78 loops over a copy, list(con), because Python does not allow removing items from a list while looping over that same list.

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 = {}    # {(key, tx): set of tx}  maps a write to its 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.setdefault((rop.key, rop.read_from_tx), set()).add(tx)  # 15
                                                                  # 16
        # detect RMW (read-modify-write) transactions             # 17
        for key in tx.keys_both_read_and_written():               # 18
            rop = tx.read_of(key)                                 # 19
            if wwpairs.get((key, rop.read_from_tx)) is not None:  # 20
                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 = {}      # {key: list of chains}                      # 29
    for tx in g.nodes:                                            # 30
        for wrop in tx.writes:                                    # 31
            chains.setdefault(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 all_pairs(chainset):              # 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] == tx1)     # 48
        chain2 = next(c for c in chains[key] if c[0] == 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(len(chain) - 1):   # i in [0, len(chain)-2]  # 56
                for rtx in readfrom.get((key, chain[i]), set()):  # 57
                    if rtx != 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.get((key, chain_i[-1])):     # tail has no readers  # 66
        edge_set = {(chain_i[-1], chain_j[0])}   # tail -> head   # 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 c in list(con):          # c = (edge_set1, edge_set2)     # 78
        if any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in c[0]):  # 79
            g.edges |= c[1]                                       # 80
            con.remove(c)                                         # 81
        elif any(tr.reaches(tx_j, tx_i) for (tx_i, tx_j) in c[1]):  # 82
            g.edges |= c[0]                                       # 83
            con.remove(c)                                         # 84
    return con, g                                                 # 85

Implementation size (from §5, Figure 4):

ComponentPartSize
Client libraryhistory recording620 lines of Java
Client librarydatabase adapters900 lines of Java
Verifierdata structures and algorithms2k lines of Java
VerifierGPU optimizations550 lines of CUDA/C++
Verifierhistory parser and others1.2k lines of Java

Limitations (what Cobra cannot do)

(from §1, §2.1, §3.5, §6.1, §6.2, §8)

  1. No time guarantee. There is no guarantee Cobra finishes in reasonable time; in the worst case the running time is in principle exponential, because the problem is NP-complete. In the authors' experiments on real workloads, it did finish. (L1)
  2. Key-value operations only. Cobra does not handle range queries or SQL operations such as join and sum unless they are first translated into key-value reads and writes. (L2)
  3. Client style. Client programs may use multiple threads but not the async / event-driven style. (L3)
  4. The checker itself must not crash. The verifier and collectors are assumed never to crash; making them fault-tolerant is left to future work. (L4)
  5. No new bugs found. The authors did not find serializability violations "in the wild." In their words: "we lack a sensational headline." (L5)
  6. Slow on write-heavy workloads. Cobra can be slow when there are many blind writes (writes with no read of the same key first), as in the BlindW-WM test. On BlindW-RW and BlindW-WM, the history eventually ran out of GPU memory in the scaling experiment. (L6)
  7. Detects, does not prevent. Cobra can only tell you a violation happened; it cannot stop it. For strict serializability it assumes the collectors' clocks differ by less than a threshold (100 ms by default); if they differ by more, it may wrongly reject a history that was actually fine. (L7)
  8. One trust domain, full history. Clients, collectors, and verifier must all be on the same trusted side, and the verifier needs the complete history of requests and responses. (L8)

After publication: not part of the paper

Snapshot dated 2026-09-30, reproduced as given in the source file. It was 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. This page was compiled without web access, so the snapshot was not updated 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. Each relation below 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

Reader profile used: 11th-grade student, one intro Python class, visual learner, wants the gist; no background in databases, graphs, solvers, or GPUs.

Ledger items included

Ledger items omitted

Additions (not the authors' words)