Verification

SystemVerilog Mailbox vs Queue

By CourseTron Team ·  6 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: A SystemVerilog queue is a passive, non-blocking data container, while a mailbox is a built-in, thread-safe FIFO whose put() and get() block when it is full or empty, which is why mailboxes, not queues, are the standard way to pass transactions between testbench processes.

    Two FIFOs, two very different jobs

    Both a queue (int q[$]) and a mailbox (mailbox #(int) mb) hand out items in first-in, first-out order, so candidates routinely confuse them. The difference is not ordering but synchronisation. A queue is a data structure: you read it, you write it, and nothing waits. A mailbox is a communication primitive: it is built into the language specifically so that one process can hand data to another and sleep until the data arrives. If you have read our guide to Verilog versus SystemVerilog, think of the mailbox as one of the class-based features that Verilog never had.

    Key fact: Queue operations never suspend a process. Mailbox put() and get() can suspend, and the simulator scheduler wakes the sleeper when the condition clears.

    Queue behaviour: fast, flexible, unsynchronised

    A queue supports push_back, push_front, pop_front, pop_back, insert, delete, indexing and slicing. You can reorder it, peek at element 5 or delete from the middle, which is why scoreboards use queues for expected-data lists that must be searched. A queue may also be bounded: int q[$:15] holds at most 16 elements (indices 0 to 15). But a bounded queue does not stall the writer. A push_back onto a full bounded queue is simply rejected with a warning, and a pop_front on an empty queue returns the default value of the type (0 for int) plus a warning. Neither case waits for the other side.

    That is a classic bug source: a consumer that pops an empty queue silently receives zeros and checks them against real data.

    Mailbox behaviour: blocking and bounded by design

    A mailbox is created with new() (unbounded) or new(N) (bounded to N entries). The methods are:

    • put(item) blocks while a bounded mailbox is full; try_put returns 0 instead of waiting.
    • get(item) blocks while it is empty and removes the item; try_get returns 0 if nothing is there.
    • peek(item) and try_peek copy the head item without removing it.
    • num() reports how many messages are currently stored.

    Mailbox new(0) means unbounded, not zero-capacity, which surprises many people. With a bound of 1, the producer cannot run more than one item ahead of the consumer, which is exactly the back-pressure you want when a driver is slower than a generator.

    Bounded mailbox new(2): who blocks and when Generator mb.put(tr) mailbox (bound 2) Driver mb.get(tr) Full: put() sleeps until the driver does a get() Empty: get() sleeps until the generator does a put() CourseTron · coursetron.com
    Two slots are filled; a third put() would block the generator, while an empty mailbox blocks the driver.

    A minimal producer-consumer example

    class txn; rand bit [7:0] data; endclass
    
    module tb;
      mailbox #(txn) mb = new(4);   // bounded to 4
    
      initial begin : producer
        repeat (10) begin
          txn t = new(); void'(t.randomize());
          mb.put(t);                // blocks if 4 are pending
        end
      end
    
      initial begin : consumer
        txn t;
        forever begin
          mb.get(t);                // blocks if empty
          #10 $display("got %0h", t.data);
        end
      end
    endmodule

    The producer finishes its fifth put only after the consumer drains one item. To emulate that with a plain queue you would need a polling loop such as wait (q.size() < 4) on the writer side and wait (q.size() > 0) on the reader side, and you must still guard against two readers racing. The mailbox does all of this inside the kernel, in simulators from Synopsys VCS, Cadence Xcelium and Siemens Questa alike.

    Key fact: Always use a typed mailbox (mailbox #(txn)). An untyped mailbox accepts any singular value, and a type mismatch only shows up as a run-time error.

    Side-by-side comparison

    PropertyQueueMailbox
    BlockingNeverput/get/peek block; try_ variants do not
    Bounded[$:N], overflow is rejected with a warningnew(N), writer waits when full
    Random access, insert, deleteYesNo (head only)
    Thread-safe hand-offNo, you add a semaphore or eventYes, built in
    Typical useScoreboard reference listsGenerator to driver, monitor to scoreboard
    What happens when the reader finds nothing? Queue: q.pop_front() empty Returns 0 + warning Reader keeps running Fake data may be checked Mailbox: mb.get(t) empty Reader suspends Wakes on next put() Only real data is seen CourseTron · coursetron.com
    Before and after: the same empty-read event is a silent error for a queue and a clean wait for a mailbox.

    Seeing it in motion

    Transactions flowing through a mailbox Producer mailbox Consumer Items wait inside, then leave in arrival order CourseTron · coursetron.com
    Animation: each transaction enters the mailbox, waits, and is delivered to the consumer in FIFO order.

    When to pick which

    Use a mailbox whenever data crosses a process boundary in a UVM-style or plain SystemVerilog bench: generator to driver, monitor to scoreboard, monitor to coverage collector. If your coverage code samples each transaction, see our walkthrough of functional coverage in SystemVerilog, and for how randomised items are produced upstream read constrained randomization. Use a queue when you need random access, searching, or out-of-order deletion, such as matching an actual packet against a list of expected ones. A common production pattern is both: a mailbox delivers the item, and the scoreboard stores it in a queue until the DUT output appears.

    Remember that UVM wraps this idea in uvm_tlm_analysis_fifo and uvm_tlm_fifo; the blocking get semantic is identical. Teams at design houses such as Avecas use the same hand-off pattern in real IP verification environments.

    Key fact: Handing a class handle through a mailbox passes a reference, not a copy. If the producer reuses and modifies the same object, the consumer sees corrupted data, so allocate a new object every iteration.

    Common pitfalls interviewers probe

    • Unbounded growth: a fast generator with new() can queue millions of objects and exhaust memory. Bound it.
    • Deadlock at end of test: a consumer stuck in get never lets the fork finish; use try_get with a timeout or disable fork.
    • Forgetting handle sharing: two threads must point to the same mailbox object; constructing it twice creates two unrelated mailboxes.
    • Using peek as get: peek leaves the item, so the loop spins on one item forever.

    Practise these on a real simulator in our CourseTron verification courses, and if you want guided interview preparation, the training team at ChipXpert runs live batches.

    Build a complete layered testbench with mailboxes, scoreboards and coverage in the CourseTron SystemVerilog verification course.

    FAQ

    Is a SystemVerilog mailbox bounded by default?

    No. new() and new(0) create an unbounded mailbox. Pass a positive integer to bound it, after which put() blocks when it is full.

    Can a queue replace a mailbox?

    Only if you add your own synchronisation with events, semaphores or wait statements. The mailbox already provides blocking and mutual exclusion.

    What is the difference between get() and try_get()?

    get() suspends the calling process until an item exists, while try_get() returns immediately with 0 when the mailbox is empty. It is the non-blocking choice inside functions.

    Does a bounded queue block when it is full?

    No. A push onto a full bounded queue is not stored and a warning is issued; the writer carries on.

    Advertisement