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_putreturns 0 instead of waiting.get(item)blocks while it is empty and removes the item;try_getreturns 0 if nothing is there.peek(item)andtry_peekcopy 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.
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
| Property | Queue | Mailbox |
|---|---|---|
| Blocking | Never | put/get/peek block; try_ variants do not |
| Bounded | [$:N], overflow is rejected with a warning | new(N), writer waits when full |
| Random access, insert, delete | Yes | No (head only) |
| Thread-safe hand-off | No, you add a semaphore or event | Yes, built in |
| Typical use | Scoreboard reference lists | Generator to driver, monitor to scoreboard |
Seeing it in motion
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
getnever lets the fork finish; usetry_getwith a timeout ordisable 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.