In short: SystemVerilog executes every time step through ordered event regions, and a race appears whenever a testbench and a DUT touch the same signal in the same region, so the fix is to drive and sample in different regions (clocking blocks, nonblocking assignments, or the Reactive region) instead of hoping for a lucky simulator ordering.
Why races exist at all
Verilog was designed as a language where many processes run "at the same time", yet a simulator is a single program that must pick an order. The standard deliberately leaves the order of ready events inside one region undefined. That freedom lets Synopsys VCS, Cadence Xcelium and Siemens Questa optimize differently, and it is exactly why a testbench that passes on one tool can fail on another. A race is not a simulator bug. It is a gap in your code that the standard allows the tool to fill any way it likes.
Interviewers love this topic because it separates people who memorized syntax from people who understand simulation semantics. If you can walk through one clock edge region by region, you can debug almost any "works in VCS, fails in Xcelium" ticket. If you are still building the foundations, start with our comparison of Verilog vs SystemVerilog.
The time-step regions you must know
Inside a single simulation time step, IEEE 1800 defines a set of regions that are processed in a fixed sequence. The ones that matter day to day are these:
- Preponed: read-only. Values are sampled here before anything changes at this time, which is how concurrent assertions get stable inputs.
- Active: blocking assignments, continuous assignments, the evaluation of the right-hand side of nonblocking assignments, and
alwaysblocks woken by an event. - Inactive: events scheduled with
#0. Avoid them; they are a classic way to hide a race. - NBA: the left-hand sides of nonblocking assignments are updated here, only after the Active and Inactive regions are empty.
- Observed: property evaluation for concurrent assertions happens here, after the design has settled.
- Reactive: program blocks and assertion action blocks run here, so testbench code reacts to settled design values.
- Postponed: read-only, used by
$strobeand$monitor.
The scheduler loops: it drains Active and Inactive, then NBA; if the NBA updates woke new events it returns to Active, and only when the whole design is quiet does it move on to Observed and Reactive. Reactive can itself schedule new Active events, which restarts the loop. That feedback is what makes the model "iterative".
Key fact: Order of events within the Active region is undefined by the standard. Anything that depends on it is a race by definition.
Anatomy of a classic DUT-versus-testbench race
Consider a DUT with always @(posedge clk) q <= d; and a module-based testbench that does this:
always @(posedge clk) begin
d = next_value; // blocking, Active region
end
Both processes wake on the same clock edge in the Active region. If the testbench process runs first, d changes immediately and the DUT's right-hand side sees the new value. If the DUT runs first, it captures the old value. Same code, two legal outcomes, and the winner can flip when you add a log line or change tool version. Hardware would never behave this way, because a real flop samples before the new data arrives.
The mirror image shows up on the sampling side. A monitor doing @(posedge clk) sample = bus.data; may read the value before or after the DUT's nonblocking update lands. Since NBA updates come after Active, the monitor usually sees the old value, which is what you want, but only if the DUT used nonblocking assignments. A DUT written with blocking assignments in a clocked block opens the race again.
See the loop in motion
The animation below follows an update token through one time step. Watch how the NBA update only fires after Active has drained, and how the Reactive region sees the settled result.
How SystemVerilog removes the race
The language gives you three tools, each shifting the testbench into a different region than the DUT.
- Nonblocking assignments in the DUT: keep every clocked RTL assignment as
<=. This gives other processes a chance to read the old value in Active before the update lands in NBA. - Program blocks: code inside a
programruns in the Reactive region, after the design has settled. Most teams moved away from them, but they explain the intent. - Clocking blocks: the modern answer. A clocking block samples inputs in the Preponed region (or with an input skew) and drives outputs through the NBA or Reactive path, so the interface behaves like a flop. See the deeper walk-through in the UVM testbench architecture article, where the virtual interface plus clocking block is the standard driver/monitor pattern.
Assertions follow the same philosophy. Because concurrent assertions sample in Preponed and evaluate in Observed, they never race with the design; our guide to SystemVerilog Assertions shows the sampling rules in practice.
| Technique | Drive region | Sample region | Race risk |
|---|---|---|---|
| Blocking drive in module TB | Active | Active | High |
| Nonblocking drive in TB | NBA | Active | Low |
| Program block | Reactive | Reactive | Low |
| Clocking block | NBA / Reactive (with skew) | Preponed | Very low |
Key fact: A clocking block default of #1step input skew means "the value just before the clock edge", which is the Preponed value. It is not a delay of one time unit.
Debugging a race in practice
When a regression behaves differently between tools or even between seeds, suspect a race first. A reliable checklist:
- Search testbench code for blocking assignments to DUT inputs in a clocked process.
- Look for
#0delays and forforkblocks that start at a clock edge without explicit ordering. - Run the same test with different process-ordering options. VCS and Xcelium both expose switches to randomize or reverse scheduling; a test that changes outcome has a race.
- Dump the waveform with delta-cycle visibility, since many races live inside a single time stamp where the wave viewer shows only the final value.
The same discipline matters when you cross language boundaries. A C model called through SystemVerilog DPI executes in whichever region invoked it, so returning a value to a signal at the wrong point reproduces the same hazard. If your team wants hands-on help hardening a testbench, the verification group at Avecas design services does this for production projects.
Frequently asked questions
Is #0 ever a good idea?
No. It moves an event to the Inactive region, which hides one race while usually creating another. Replace it with a proper clocking block or event.
Does the NBA region guarantee RTL is race-free?
Only for synchronous logic that follows the usual rule: nonblocking assignments in clocked blocks, blocking in combinational ones. Mixing them on the same variable or clocking a flop from a gated or derived clock inside the Active region can still race.
Why does my monitor sample the old value at posedge?
Because the DUT's flop output updates in NBA, after your Active-region read. That is correct hardware behaviour, but it breaks if the DUT uses blocking assignments.
Are program blocks deprecated?
They are not removed, but UVM environments rely on clocking blocks and interfaces, so you rarely need them in new code.
Want to master region semantics, clocking blocks and UVM drivers with real labs? Explore the verification tracks in our CourseTron courses, or train with mentors at ChipXpert.