Skip to content

Why bitemporal?

A typical CAM-table store records “MAC X is on port Y.” A bitemporal one records “we believed at time R that MAC X was on port Y for the wire-time range V.” That second clause is the whole point.

Bitemporal storage has two independent ranges per row:

  • valid_during — the wire-time range during which the fact was true on the network. “This MAC was on this port from 10:00 to 10:05.”
  • recorded_during — the belief-time range during which the system believed it. “From 10:00:02 (when we ingested the event) onwards we recorded this as true; we still believe it as of now.”

Most rows have one row per fact: a brand-new observation closes the previous row’s valid_during and inserts a new row whose recorded_during opens at now().

The interesting case is belief revision — when a late event arrives saying “actually, what you believed at T was wrong.” Then we close the old row’s recorded_during at now() and insert a new row with the corrected valid_during and a fresh recorded_during. The original row stays in the log forever; nothing is overwritten.

A timeline of one MAC’s events:

T ─────10:00───10:05───10:10───10:15───10:20──→
[Eth1 ]
[Eth2 ]
Event 1 (arrives at 10:00): "MAC seen on Eth1"
Event 2 (arrives at 10:05): "MAC seen on Eth2" → closes Eth1's valid_during at 10:05

Resulting rows:

entry_id=1 port=Eth1 valid_during=[10:00, 10:05) recorded_during=[10:00, ∞)
entry_id=2 port=Eth2 valid_during=[10:05, ∞) recorded_during=[10:05, ∞)

Now suppose at 10:20 a late event arrives:

Event 3 (arrives at 10:20): "MAC seen on Eth7 at 10:03" → belief revision

The old beliefs about [10:00, 10:05) on Eth1 are now contradicted at 10:03. We close entry_id=1’s recorded_during at 10:20 and insert two new rows:

entry_id=1 port=Eth1 valid_during=[10:00, 10:05) recorded_during=[10:00, 10:20) ← closed
entry_id=2 port=Eth2 valid_during=[10:05, ∞) recorded_during=[10:05, ∞)
entry_id=3 port=Eth1 valid_during=[10:00, 10:03) recorded_during=[10:20, ∞) ← new belief: Eth1 only until 10:03
entry_id=4 port=Eth7 valid_during=[10:03, 10:05) recorded_during=[10:20, ∞) ← new belief: Eth7 inserted

Now you can ask either question:

  • “What was the path at 10:04, given what we believe today?”entry_id=4 (port Eth7).
  • “What was the path at 10:04, given what we believed at 10:18?”entry_id=1 (port Eth1) — recorded_during=[10:00, 10:20) contains 10:18, and valid_during=[10:00, 10:05) contains 10:04.

Both answers are correct. Bitemporal lets you ask either.

Most CAM-table stores use one time dimension (“last_seen” + soft “history” that gets overwritten when a new value lands). That works fine for “where is MAC X right now?” but falls apart on:

  1. Late arrivals. SNMP polls have a 60-second-or-so floor; SSH snapshot diffs lag worse. A poll at 10:05 reporting “MAC on Eth7 at 10:03” arrives after the streaming-gNMI event “MAC on Eth2 at 10:05.” Single-timestamp models silently overwrite, leaving the bitemporal log saying the MAC was never on Eth7. Worse, the overwrite is undetectable later.

  2. Audit and forensic queries. Compliance and “did we know X at the time?” questions need the historical belief, not the current belief. A single-time-axis store cannot answer them.

  3. Disagreement detection. Two sources writing into a single-timestamp model fight over the row. Two sources writing into a bitemporal model produce two rows the system can compare and surface.

The valid_during / recorded_during naming and the two-axis modelling in this project mirrors what Martin Fowler called valid time and transaction time in his “Time Narratives” essay — the canonical accessible primer on bitemporal data:

Fowler, Time Narratives

If this is your first contact with the model, read Fowler’s essay first; this page picks up where it leaves off and grounds the abstraction in the specifics of MAC tables.

  • The mac_observation table has an EXCLUDE constraint that prevents two currently-believed open observations from overlapping valid_during within a single source — see the data model reference.
  • Cross-source disagreement is allowed, by design — surfaced via the disagreement view.
  • The TUI’s as-of header drives valid_during @> :audit_time filters on every screen — see querying state as of a past time.
  • Late-arriving events go through a separate code path in the reconciler — see late-arrival classification.