Mailbox in System Verilog
In short: A mailbox in SystemVerilog is a built in class that lets two parallel processes pass data safely, like a FIFO with a producer on one side and a consumer on the other. The producer calls put() to add an item and the consumer calls get() to take one. put() and get() block when the mailbox is full or empty; the try_ versions do not block. A mailbox can be bounded (limited depth) or unbounded, and it can be type parameterised for safety.
In a testbench a generator often builds transactions while a driver sends them to the design. These run in parallel, so they need a safe way to hand data across. A mailbox is that hand off. It holds items in order and manages the waiting for you, so the producer and consumer never step on each other.
A simple way to picture it
Think of a pass through window between a kitchen and a dining room. The cook places finished plates on the window ledge, and the waiter picks them up. If the ledge is full, the cook waits for space. If the ledge is empty, the waiter waits for the next plate. Neither one needs to know what the other is doing moment to moment; the ledge keeps the flow orderly. The mailbox is that window, the cook is the producer, and the waiter is the consumer.
Mailbox methods
A mailbox is a built in class with a small set of methods. The blocking ones pause the calling process until the action can complete. The non blocking ones return right away with a success flag.
| Method | Blocking? | What it does |
|---|---|---|
new(bound) | n/a | Creates a mailbox. A bound of 0 means unbounded. |
put(item) | Yes | Adds an item; waits if the mailbox is full |
get(item) | Yes | Removes an item; waits if the mailbox is empty |
peek(item) | Yes | Copies the front item without removing it |
try_put(item) | No | Adds if there is room; returns 0 if full |
try_get(item) | No | Removes if available; returns 0 if empty |
num() | No | Returns the current number of items |
Bounded vs unbounded mailbox
When you create a mailbox with new(N), N sets the maximum number of items. A bounded mailbox becomes full at N items, so a put() beyond that waits until the consumer frees a slot. Creating with new() or new(0) makes it unbounded, so put() never waits for space.
A bound is useful when you want back pressure, that is, you want a fast producer to slow down and match a slower consumer. Leave it unbounded when the producer should never be held back.
Example 1: generator and driver with a parameterised mailbox
This is the pattern you will use most: a generator randomises transactions and puts them in a mailbox, while a driver gets them and sends them on. Using a parameterised mailbox mailbox#(transaction) means the tool checks the type at compile time, so you cannot accidentally put the wrong object in.
class transaction;
rand bit [7:0] addr;
rand bit [7:0] data;
endclass
class generator;
task run(int n, mailbox #(transaction) mb);
transaction tr;
repeat (n) begin
tr = new();
if (!tr.randomize())
$error("randomize failed");
mb.put(tr); // blocks if mb is full
$display("GEN: put addr=%02h data=%02h", tr.addr, tr.data);
end
endtask
endclass
class driver;
task run(int n, mailbox #(transaction) mb);
transaction tr;
repeat (n) begin
mb.get(tr); // blocks if mb is empty
$display("DRV: got addr=%02h data=%02h", tr.addr, tr.data);
end
endtask
endclass
module tb;
initial begin
mailbox #(transaction) mb = new(); // unbounded
generator gen = new();
driver drv = new();
fork
gen.run(3, mb);
drv.run(3, mb);
join
end
endmoduleThe generator and driver run in the same fork join. When the generator puts a transaction, the driver that is waiting in get() wakes up and processes it. This is a clean producer and consumer link.
Example 2: back pressure with a bounded mailbox
Here the mailbox holds only two items. The producer runs fast but is forced to wait whenever the buffer is full, so it naturally matches the slower consumer.
module bounded_demo;
mailbox #(int) mb = new(2); // depth of 2
initial begin
fork
// Producer: tries to push 5 items quickly
begin
for (int i = 0; i < 5; i++) begin
mb.put(i); // waits when 2 items are pending
$display("[%0t] put %0d (num=%0d)", $time, i, mb.num());
end
end
// Consumer: reads one every 10 ns
begin
int v;
repeat (5) begin
#10ns;
mb.get(v);
$display("[%0t] got %0d", $time, v);
end
end
join
end
endmoduleExample 3: non blocking check with try_get
Sometimes a process should not stop and wait. Use try_get() to peek for work without blocking; it returns 0 when the mailbox is empty so the process can do something else and check again later.
module nonblock_demo;
mailbox #(int) mb = new();
initial begin
int v;
if (mb.try_get(v))
$display("got %0d", v);
else
$display("mailbox empty, doing other work"); // this branch runs
end
endmoduleExpected output, in plain words
In example 1 you see three GEN lines and three DRV lines interleaved, each matching pair sharing the same addr and data, because the driver takes exactly what the generator put. In example 2 the producer prints its first two puts right away, then pauses; each time the consumer takes one every 10 ns, the producer is freed to put the next, so puts and gets alternate. In example 3 the mailbox is empty, so try_get returns 0 and the empty branch prints.
Note: these outputs describe what the code is written to produce from a read of the IEEE 1800 LRM. Run it on your own simulator or EDA Playground to confirm.
Generic vs parameterised mailbox
A plain mailbox is type less: it can hold any type, and you get flexibility at the cost of type checking. A parameterised mailbox#(type) only accepts one type, so the compiler catches a wrong put. For testbench work, prefer the parameterised form because it removes a whole class of bugs.
| Point | mailbox | mailbox#(type) |
|---|---|---|
| Types allowed | Any type | One fixed type |
| Type checking | At run time only | At compile time |
| Best for | Quick or mixed data | Most testbench code |
When to use a mailbox
- You have a producer and a consumer running in parallel that must pass data in order.
- You want built in waiting so the producer blocks when full and the consumer blocks when empty.
- You want back pressure between a fast producer and a slow consumer (use a bounded mailbox).
- You want a clean hand off between a generator and a driver, or a monitor and a scoreboard.
Common mistakes to avoid
- Forgetting to call
new(): using a null mailbox handle gives a run time error. - Mixing put and get counts: if the consumer expects more items than the producer sends, its
get()blocks forever. - Using a type less mailbox by habit: prefer
mailbox#(type)so type errors are caught early. - Confusing
peek()andget(): peek copies without removing, so a loop on peek without get never drains the mailbox. - Assuming
put()never waits: on a bounded mailbox it does wait when full.
Mailbox vs queue
A queue is a plain data structure you index directly, with no built in waiting or thread safety. A mailbox is built for communication between parallel processes and manages the blocking for you. Use a queue for storage inside one process, and a mailbox to pass data between two processes.
To coordinate the processes that share a mailbox, see our guide on fork join. If you need mutual exclusion instead of data passing, read about the semaphore. For the data structures a mailbox carries, see array types and queues. More in our SystemVerilog tutorials.
Frequently asked questions
What is a mailbox in SystemVerilog?
A mailbox is a built in class that lets two parallel processes pass data safely, like a FIFO with a producer and a consumer. The producer calls put to add items and the consumer calls get to take them.
What is the difference between put and try_put?
put is blocking: it waits if the mailbox is full. try_put is non blocking: it adds the item if there is room and returns a nonzero value, or returns 0 immediately if the mailbox is full.
What is the difference between a bounded and unbounded mailbox?
A bounded mailbox is created with new(N) and holds at most N items, so put waits when it is full. An unbounded mailbox is created with new() and never blocks a put for lack of space.
What is the difference between get and peek?
get removes and returns the front item. peek copies the front item without removing it, so the item stays in the mailbox for the next reader.
What is the difference between a mailbox and a queue?
A queue is a plain data structure with no built in waiting. A mailbox is built for communication between parallel processes and blocks automatically when full or empty. Use a queue inside one process and a mailbox between two.
Why use a parameterised mailbox?
mailbox#(type) accepts only one type, so the compiler catches a wrong put at compile time. A plain mailbox is type less and only checks at run time, which is more error prone.

