Why is one flip-flop not enough to bring a signal into another clock domain?
A flop sampling a signal that changes near its clock edge can go metastable and hold an invalid level for an unbounded time. A second flop gives it a full period to settle, which fixes metastability but not cycle alignment, so a multi-bit bus needs a handshake or an asynchronous FIFO rather than a synchroniser per bit.
What the interviewer is scoring
- Does the candidate describe metastability as an unbounded settling time rather than as a wrong value
- That you can say precisely what the second flop buys and what it leaves unsolved
- Whether you recognise that per-bit synchronisers on a bus produce a value that existed in neither domain
- Whether a narrow pulse crossing into a slower domain is identified as a separate problem from metastability
- Whether the answer reaches a handshake or an asynchronous FIFO rather than stopping at the two-flop cell
Answer
Short answer
One flip-flop is not enough for a clock-domain crossing because a signal sampled near the clock edge can go metastable and feed invalid levels into logic. A two-flop synchronizer gives a single-bit level time to settle, but buses, pulses, and ordered data need handshakes, Gray-coded pointers, or asynchronous FIFOs.
What goes wrong with one flop
A flip-flop is specified to produce a valid output only if its data input was stable for the setup time before the clock edge and the hold time after it. That window is a promise the flop's designer makes conditionally, and a signal generated by an unrelated clock cannot keep it. Sooner or later the transition lands inside the window.
When that happens the flop does not simply pick the wrong value, which would be tolerable. It can enter a metastable state, where the internal feedback loop is balanced between the two stable levels and the output sits somewhere between a valid one and a valid zero. It will resolve, because the state is not truly stable, but the time it takes to resolve is not bounded. It is a probability distribution with a long tail, and the useful consequence is that you can make the probability of a late resolution arbitrarily small but never zero.
With a single flop, that intermediate level is presented directly to your logic. Downstream gates interpret an invalid level however their thresholds happen to fall, so two gates fed from the same metastable output can disagree about what they saw. That is what makes it worse than sampling the wrong value: sampling the wrong value gives you a design where a signal arrives one cycle later than you expected, which most protocols already tolerate. Metastability gives you a design where the same signal is simultaneously one and zero in different places.
What the second flop does
The standard fix is to sample the signal twice in the destination clock, with nothing between the two flops but a direct connection.
// Illustrative two-flop synchroniser for a single-bit level.
// Asynchronous active-low reset, sampled entirely in clk_dst.
reg sync_q1, sync_q2;
always @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) begin
sync_q1 <= 1'b0;
sync_q2 <= 1'b0;
end else begin
sync_q1 <= async_in; // this flop is the one allowed to go metastable
sync_q2 <= sync_q1; // it has had a full clk_dst period to resolve
end
end
// Only sync_q2 may be read by the rest of the destination logic.
assign safe_level = sync_q2;
The first flop is deliberately sacrificial. Its output may be invalid immediately after the edge, and it is given the whole clock period before the second flop looks at it. Because the resolution time distribution falls away steeply, that period converts an unacceptable failure rate into one expressed as a mean time between failures long enough to be irrelevant for the part's life. Adding a third flop extends the settling window further, which is why deeply pipelined designs at high frequency sometimes use one.
Two things about that snippet matter more than they look. The intermediate signal must feed nothing but the second flop, because any other consumer is reading a possibly metastable value and reintroduces exactly the disagreement you were preventing. And the two flops must be physically close, so the wire between them consumes as little of the settling window as possible; this is why synchronisers are usually written as a recognisable cell and treated specially in implementation rather than left to general optimisation.
What the second flop does not do
It does not tell you when the signal arrives. The transition is registered on either the edge it nearly missed or the following one, so the destination sees it after an uncertainty of about one destination clock period on top of the two-flop delay. Any design that assumes a fixed number of cycles between an event in one domain and its appearance in another is wrong regardless of how many synchroniser flops you use.
It does not guarantee the transition is seen at all. A synchroniser samples a level. If the source asserts a signal for one source-clock cycle and the source clock is faster than the destination clock, the pulse can begin and end entirely between two destination edges and simply never be observed. This is a separate defect from metastability and no number of flops fixes it. The remedy is to change what crosses: either stretch the pulse in the source domain so it is guaranteed wider than a destination period, or convert it to a level that toggles and stays put until the destination acknowledges it.
And it does not make anything about the crossing free. Every crossing costs latency and adds a place where a design review has to establish that the protocol on both sides tolerates the uncertainty.
Why a bus needs a different mechanism entirely
The instinct on seeing an eight-bit value crossing a domain boundary is to instantiate eight synchronisers. This produces a specific and nasty bug, and the reason is worth stating carefully: each synchroniser resolves independently. When several bits change on the same source cycle, some of their first-stage flops catch the change on one destination edge and others catch it on the next. For one destination cycle the bus reads as a mixture of the old value and the new one, which is a value that was never present in the source domain and may be a legal but wrong state as far as the destination is concerned. A counter stepping from 0111 to 1000 can be read as 1111 or 0000.
There are three honest answers, and which you choose depends on what is crossing.
If the value is a counter or otherwise changes by one step at a time, Gray code it so exactly one bit differs between consecutive values. Then a skewed sample can only return the old value or the new one, never a third. This is the reason asynchronous FIFO read and write pointers are Gray coded: the pointer comparison that generates full and empty is doing a crossing, and Gray coding makes a stale sample merely conservative rather than wrong.
If the value is arbitrary data sent occasionally, use a handshake and do not synchronise the data at all. The source drives the bus, holds it, and then asserts a single request bit. Only that one bit goes through a synchroniser. When the destination sees the request, the data has been stable for at least the synchroniser's delay, so it can be captured directly. The destination returns an acknowledgement through a synchroniser in the other direction, and only then may the source change the bus.
sequenceDiagram
participant S as Source domain
participant R as Req synchroniser
participant D as Destination domain
participant A as Ack synchroniser
S->>S: Drive bus and hold it stable
S->>R: Assert req
R->>D: req arrives settled
D->>D: Capture bus directly
D->>A: Assert ack
A->>S: ack arrives settled
S->>S: Release bus and drop reqNotice that the data bus never enters a synchroniser in that sequence. The whole correctness argument is that a single-bit control signal is the only thing crossing, and the bus is simply held still for longer than the crossing takes.
If the value is a stream, use an asynchronous FIFO: a dual-port memory written in one domain and read in the other, with Gray-coded pointers crossing between them. It combines both of the previous ideas and it is the mechanism you should name for continuous throughput, because a handshake per word limits you to one transfer every several cycles.
The claim you are quietly making when you constrain the crossing
Static timing analysis will report the path into the first synchroniser flop as failing, because the launch and capture clocks are unrelated and their relationship is undefined. Telling the tool to ignore that path is a normal and necessary step, and it is also the moment where discipline is usually lost. What you have asserted is that this specific crossing is handled by a synchroniser, and the tool now believes you for every path you covered.
If the exception is written broadly enough to cover paths that were not synchronised, or if a later change routes a new signal across the same boundary without one, the report stays clean and the design is broken. The failure surfaces as an intermittent fault whose rate depends on temperature and voltage, which is close to the worst debugging experience the discipline offers.
The two-flop synchroniser solves exactly one problem, an invalid level propagating into your logic; cycle alignment, narrow pulses and multi-bit coherency are three separate problems that each need their own mechanism.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- Why are the pointers of an asynchronous FIFO Gray coded rather than binary?
- How would you get a single-cycle pulse from a fast domain safely into a slower one?
- What goes wrong if the output of the first synchroniser flop is used by any logic other than the second flop?
- How do you tell static timing analysis to stop reporting the path into the first synchroniser flop, and what have you asserted by doing so?
Related questions
- Your timing report shows a setup violation on a critical path. What do you do about it?mediumAlso on rtl-design6 min
- The trigger for the outage is gone and you have restarted the service, and it collapses again within a minute of taking traffic. Why will it not recover on its own?hardAlso on metastability7 min
- A transform has been writing wrong revenue figures for three days and six downstream tables have consumed it. How do you backfill the corrected data without double-counting anything?hardSame kind of round: design4 min
- Your consumer-driven contract test passes in CI, but production rejects a request because a supposedly optional field is missing. What did the contract testing actually miss?hardSame kind of round: concept4 min