Introduction

Shared data is everywhere in concurrent systems: a kernel clock that a hundred threads read every millisecond, a configuration struct updated once a minute. The classic approach — a mutex or reader-writer lock — protects both sides, but often at the cost of making writers wait for readers to finish first.

A seqlock (sequence lock) flips the deal. Writers never block on readers. Instead, each write increments a shared sequence counter before and after the update. Readers snapshot the counter at the start and again at the end of their read; if the two snapshots differ, a write slipped in and the reader simply retries.

The key insight: checking consistency is cheap, and writes are rare. So instead of blocking the rare writer, you make the common reader pay a tiny retry cost on the occasions it loses the race. The Linux kernel has used this pattern for reading the system clock since 2003.

Try It

The demo below shows a single shared value protected by a seqlock. Click Start writer to begin writing new values. Click Read value to attempt a read — the reader snapshots the sequence counter, reads the data, then checks whether the counter changed.

<!-- {{c_html_intro}} -->
<div class="panel">
  <div class="seq-row">
    <span class="label">{{lbl_seq}}</span>
    <span id="seqDisplay" class="seq-val">0</span>
    <span id="seqStatus" class="seq-note"></span>
  </div>
  <div class="data-row">
    <span class="label">{{lbl_data}}</span>
    <span id="dataDisplay" class="data-val">—</span>
  </div>
</div>
<div class="log-area" id="log" aria-label="{{lbl_log}}"></div>
<div class="btns">
  <button id="btnWrite" type="button">{{btn_start_writer}}</button>
  <button id="btnRead" type="button">{{btn_read}}</button>
  <button id="btnReset" type="button" class="ghost">{{btn_reset}}</button>
</div>
/* {{c_css_intro}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.panel { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 10px;
         padding: .7rem 1rem; margin-bottom: .6rem; display: flex; flex-direction: column; gap: .35rem; }
.seq-row, .data-row { display: flex; align-items: baseline; gap: .5rem; }
.label { font-size: .8rem; color: #556; min-width: 5rem; }
.seq-val { font: 700 1.6rem ui-monospace, monospace; color: #1d3557; min-width: 2.5rem; }
.seq-note { font-size: .8rem; color: #c92f3c; font-weight: 600; }
.seq-note.ok { color: #0a7d33; }
.data-val { font: 600 1.1rem ui-monospace, monospace; color: #444; }
.log-area { height: 180px; overflow-y: auto; border: 1px solid #cdd9e3; border-radius: 8px;
            padding: .4rem .6rem; font-size: .82rem; font-family: ui-monospace, monospace;
            background: #f8fafc; margin-bottom: .6rem; }
.log-area p { margin: .15rem 0; line-height: 1.35; }
.log-area p.ok { color: #0a7d33; }
.log-area p.bad { color: #c92f3c; }
.log-area p.info { color: #555; }
.log-area p.write { color: #1d3557; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
// Code not found

Notice: when the counter is odd at snapshot time, a write is in progress and the reader immediately retries without using the data. When the two counter snapshots differ, a write completed during the read and the reader retries again. Only when both snapshots are equal and even does the reader accept the value.

The Real Complexity

Seqlocks look simple, but the details matter:

  • Writer exclusion: only one writer at a time is allowed (writers still lock each other out). The counter being odd signals "write in progress"; readers spin-retry rather than wait.
  • Reader retry cost: if writes happen faster than reads complete, a reader could retry indefinitely. Seqlocks are only efficient when writes are rare relative to the read time.
  • Torn reads: the reader may see partially-updated data during a retry. This is safe only if the data type is self-consistent even mid-write — or if the retry discards the torn snapshot before any pointer dereference.
  • Memory ordering: on modern CPUs, loads and stores can be reordered. Correct seqlock implementations surround the critical sections with memory barriers (or acquire/release atomics) so the counter snapshot and the data reads are not reordered past each other.
  • No pointer or complex objects: seqlocks are unsafe for data containing pointers or structures that could be freed while a reader holds a stale reference — the reader might dereference freed memory before noticing the sequence mismatch.

For plain numeric or struct data that is updated atomically (like a timestamp or a clock), seqlocks are among the fastest synchronization primitives available — O(1)O(1) writer, O(r)O(r) reader where rr is the number of retries, typically r=0r = 0 or 11 under normal load.

Where It Matters

Seqlocks appear wherever reads vastly outnumber writes and writer latency must be minimized:

  • Linux kernel timekeeping: xtime (wall-clock time) and jiffies_64 are protected by seqlocks. Thousands of processes read the time per second; the update happens once per timer tick.
  • Network statistics: byte and packet counters are read by monitoring tools constantly but written only when a packet arrives. Seqlocks let the writer path stay lock-free.
  • Real-time systems: any shared state that a high-priority thread writes but many lower-priority threads read can use a seqlock to avoid priority inversion — the writer never blocks on a reader.
  • Configuration hot-reloading: a seqlock protects a config struct that is read millions of times per second but updated once per minute.

The pattern also inspired sequence counters (seqcount_t in Linux) — a seqlock without writer mutual exclusion, for cases where external locking already ensures only one writer runs at a time.

Seqlocks pair naturally with other concurrency primitives: use them for the read-hot data, and a regular mutex for everything else.

Conclusion

The seqlock's elegance is in what it removes: the lock on the reader's path. By encoding writer progress in a sequence counter, readers can detect interference and simply retry — turning a blocking protocol into a spin-retry one that keeps the fast path free for writers.

It is a reminder that the best synchronization primitive for a workload is not always the most general one. When reads dominate and writes are rare, the cost of the occasional retry is far cheaper than making every writer wait. That insight — push the cost to the rare event — echoes through lock-free data structures, optimistic concurrency in databases, and beyond. See also P vs NP for a broader look at how the shape of a problem determines which approach wins.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/seqlocks/Content licensed under CC BY-NC 4.0.