The 3.5T Rule: Modbus Timing Requirements That Kill RS-485 Networks

Why the 3.5-character silent interval in the Modbus serial line specification is the most violated timing requirement in industrial automation — and how oms-modbus enforces it at the protocol level.

OrangeHorse Engineering Team 8 min read

The Modbus Serial Line Protocol specification has exactly one hard timing requirement: section 1.4, the 3.5-character silent interval between frames. It is the rule that separates a reliable Modbus implementation from one that works in the lab and fails in the field. And it is the rule that almost every open-source Modbus library ignores.

oms-modbus enforces it. Here is why that matters, what happens when you skip it, and how we implemented spec-compliant timing without sacrificing throughput.

What the Spec Actually Says

From the Modbus over Serial Line Protocol Specification (V1.02):

A silent interval of at least 3.5 character times must separate Modbus messages.

A "character time" is the time required to transmit one byte (11 bits in standard Modbus RTU: 1 start bit, 8 data bits, 1 parity bit, 1 stop bit). So the minimum silent interval in seconds is:

T = 3.5 × 11 / baud_rate

At common baud rates, this works out to:

Baud RateRaw 3.5 × 11 bits ÷ baudEnforced spacing
9,6004.01 ms4.01 ms
19,2002.01 ms2.01 ms
38,4001.00 ms1.75 ms
57,6000.67 ms1.75 ms
115,2000.33 ms1.75 ms

Above 19,200 baud, the spec says to use a fixed 1.75 ms interval. The logic is simple: at higher speeds, the character time shrinks to the point where a 3.5-character gap is hard to detect reliably on a UART, so a fixed floor prevents false frame detection.

What Happens When You Violate It

The 3.5T rule is not an optimization hint. It is the framing mechanism for the entire protocol. Modbus RTU has no start-of-frame delimiter, no length prefix, and no end-of-frame marker. The only way a receiver knows where one frame ends and the next begins is the silent interval.

If a transmitter sends two frames with less than 3.5T between them, the receiver merges them into a single frame. It reads the first frame's bytes, then immediately reads the second frame's bytes as a continuation of the same message. The CRC at the end of the merged blob does not match, so the receiver discards both frames. Two valid messages are lost — not because of noise, not because of a bad cable, but because of a timing violation at the transmitter.

On a lightly loaded bus with one sensor, you might never notice. The operating system's serial buffer and driver scheduling usually insert enough delay between application-level writes that the 3.5T rule is satisfied by accident. But load the bus with 30 sensors and a polling cycle of 100 ms, and the accidental delays disappear. The OS serial buffer fills up, the driver flushes frames back-to-back, and suddenly 20% of your readings are CRC errors.

The False Positive Mirror: Inter-Frame Gap vs. Intra-Frame Gap

The 3.5T rule also governs the intra-frame gap. If a transmitter pauses for more than 1.5 character times within a single frame, the receiver interprets that pause as the end of the frame, then immediately receives the remaining bytes as the start of a new (garbage) frame. Both are discarded.

This is why Modbus RTU over half-duplex RS-485 is harder than it looks. You must:

  1. Transmit the entire frame without pausing for more than 1.5T between bytes
  2. Then wait at least 3.5T before starting the next frame

A UART in FIFO mode handles (1) automatically — the hardware shifts bytes out back-to-back. But (2) is the application's responsibility. Most Modbus libraries leave it to the operating system, which means it works on Linux (whose tty layer has a configurable inter-character timeout), might work on Windows (depending on the USB-to-serial driver quality), and definitely does not work predictably on embedded RTOS targets.

How oms-modbus Does It

oms-modbus treats the 3.5T rule as a protocol-level contract, not an OS-level convenience. The spacing is computed once by BusTiming and enforced on every frame by the transport layer. You set it with one line on the client:

// Bus timing is on by default (3.5T @ 9600 baud). Override to match your bus:
let opts = ClientOptions::default()
    .with_bus_timing(BusTiming::rtu_35t(9600));
let client = rtu::with_options(port, opts);

The spacing itself is computed with integer arithmetic in BusTiming::rtu_35t (from src/bus_timing.rs):

let min_spacing_us = if baud_rate > 19_200 {
    1_750                                    // fixed floor above 19.2 kbaud
} else {
    (3_500_000u64 * 11) / baud_rate.max(1) as u64  // 3.5 × 11 bits ÷ baud, in µs
};

Under the hood, SniffIo — the transport wrapper — records a timestamp on every read and write (BusTiming::touch()), and before each write it awaits BusTiming::wait_if_needed(), which measures the elapsed time since the last bus activity and waits for the remaining gap. Three design decisions worth noting:

1. Async sleep, not busy-wait

wait_if_needed() uses tokio::time::sleep() — an asynchronous wait that parks the task and yields the executor — rather than a spin_loop() busy-wait. On a diagnostic tool polling several buses at once, or a gateway juggling Modbus plus an MQTT connection, spinning the CPU for the silent interval would starve every other task. sleep lets the runtime schedule other work while the gap elapses, and its sub-millisecond timer precision is more than enough for a 1.75 ms floor.

2. Integer arithmetic, no floating point

The spec formula is 3.5 × 11 / baud_rate, which produces a floating-point result. Floating-point arithmetic in timing code is dangerous — rounding errors at nanosecond scales produce silent-interval jitter that is invisible in unit tests but adds up over millions of frames. rtu_35t computes the spacing directly in microseconds with integers: (3_500_000 × 11) / baud_rate, where 3_500_000 is "3.5" scaled by a million to avoid the fraction. No rounding anywhere.

3. Configurable, not hard-coded

Some Modbus devices — particularly older PLCs — require longer silent intervals than the spec mandates. BusTiming::custom(Duration::from_millis(50)) sets an arbitrary minimum gap. BusTiming::rtu_35t_raw(baud) returns the uncapped formula at high baud rates (e.g. ~334 µs at 115,200) for the rare case where you need the raw 3.5T instead of the 1.75 ms floor.

The Throughput Question

The obvious objection: "If you wait 1.75 ms after every frame, how fast can you poll?"

At 115,200 baud, a Read Holding Registers request is 8 bytes and a single-register response is 7 bytes. Each frame is followed by the silent interval — and at 115,200 baud that interval is the spec's fixed 1.75 ms floor. For a single register read:

  • Request: 8 bytes × 11 bits ÷ 115,200 = 764 µs
  • Response: 7 bytes × 11 bits ÷ 115,200 = 668 µs
  • Silent intervals (2): 2 × 1,750 µs = 3,500 µs
  • Total per transaction: ~4.93 ms

That is about 200 register reads per second on a dedicated bus. Notice the silent intervals dominate: 3.5 ms of waiting against 1.43 ms of actual transmission. At high baud rates the 1.75 ms floor — not the transmission — is the limiting factor, which is exactly why the spec fixes it at 1.75 ms instead of letting it shrink to the ~334 µs the raw formula would give.

For a 30-sensor station polling 10 registers per sensor per second (one FC03 read of 10 registers each), each transaction is ~6.7 ms, so the bus spends ~200 ms of every second talking — about 20% utilization. The 3.5T rule is not the bottleneck. Bad CRC handling and retry storms are.

Testing Against the Spec

oms-modbus's timing is covered in the 440+ test suite by a dedicated bus_timing integration test module. The tests assert that:

  1. BusTiming::rtu_35t returns the spec value at every baud rate — 3.5 × 11 / baud below 19,200, and the fixed 1,750 µs floor above it
  2. BusTiming::custom returns the exact configured gap
  3. wait_if_needed() returns immediately on the first call (no history), then delays at least min_spacing after touch()

The delay test measures the actual elapsed time with tokio::time::Instant, so it catches a timing path that returns early. Timing-dependent tests are inherently flaky on shared CI hardware, so the suite uses a modest 10 ms spacing for the behavioral test and relies on deterministic value assertions for the rest.

What This Means for Your Bus

If you are integrating Modbus sensors into a system — whether using our OHTS-series environmental sensors or any other vendor's devices — check your Modbus library's timing behavior. Ask three questions:

  1. Does it enforce the 3.5T rule? If it relies on the OS serial driver to insert delays, it will fail on a loaded bus.
  2. Does it detect framing errors? A library that silently discards CRC-mismatched frames without logging is hiding bus problems.
  3. Can you measure the actual inter-frame gap? Without timestamped frame logs (see WireTap), you cannot verify that the library's timing is correct.

oms-modbus answers yes to all three. If your current Modbus library does not, consider whether the bus errors you are chasing are actually software bugs masquerading as hardware problems.


Read the Modbus Serial Line Protocol Specification V1.02 for the complete timing requirements. The 3.5T rule is in section 1.4, and it is worth reading the original text — most implementations get the baud-rate threshold wrong.

modbus rs485 bus-timing 3.5T serial-protocol industrial diagnostic