Press Releases

J1939 Transport Protocol Explained

September 3, 2026

Most J1939 introductions stop at "BAM broadcasts, RTS/CTS is point-to-point." That's true, but it's the surface of the Transport Protocol (TP) layer — and it's not the part that actually causes bugs in production. The part that causes bugs is timers expiring at the wrong moment, abort handling that's incomplete, sequence numbers that roll over unexpectedly, or two TP sessions colliding on the same bus. This article goes past the basic comparison into the parts of the TP layer that matter when you're actually implementing or debugging it.

Why TP Exists (Briefly)

A standard CAN frame carries at most 8 data bytes. J1939 defines PGNs that can carry up to 1,785 bytes (VIN strings, calibration data, software version info, multi-fault DM1 messages, etc.). The Transport Protocol layer, defined in J1939-21, exists to fragment that oversized payload into a sequence of 8-byte CAN frames and reassemble it correctly on the other end — while handling the reality of a shared, unreliable, multi-node bus.

There are two transfer modes, and the choice between them isn't just "broadcast vs. specific" — it changes the entire flow control model:

Aspect BAM (Broadcast Announce Message) RTS/CTS (Connection Mode)
Destination Global (255) Specific node
Flow control None — sender paces itself Receiver dictates pace via CTS
Max packets between pauses Fixed rate, ~50-200 ms between frames Negotiated per CTS response
Can be interrupted/aborted No formal abort mechanism Yes, via Connection Abort message
Multiple simultaneous sessions Any number of listeners, no acknowledgment One session per source/destination pair at a time
End confirmation None End of Message ACK

The Timers That Actually Govern TP Behavior

J1939-21 defines several timeout values that control how long a sender or receiver waits before declaring a session dead. These aren't optional — a compliant stack has to implement all of them, and getting them wrong is one of the most common sources of intermittent TP failures.

Timer Typical Value Applies To What Happens on Expiry
T1 750 ms Waiting for CTS after sending RTS Sender aborts the connection
T2 1250 ms Waiting for next CTS after a burst of Data Transfer frames Sender aborts the connection
T3 1250 ms Receiver waiting for the next Data Transfer packet Receiver aborts the connection
T4 1050 ms Waiting between bursts of Data Transfer frames when new CTS is expected Sender aborts the connection
Tr (response time) 200 ms Receiver's max time to respond to RTS with CTS Sender should not assume success without a response
Th (holding time) 500 ms Interval between BAM Data Transfer packets Not strictly enforced but recommended minimum spacing

Practical implication: a stack that only implements "send RTS, wait forever for CTS" isn't spec-compliant and will hang indefinitely on a non-responsive node instead of cleanly aborting and freeing the session. This is the single most common gap in hobbyist or minimal TP implementations — the happy path works, but there's no timeout-driven cleanup.

Connection Abort: The Part Most Guides Skip

RTS/CTS sessions can be aborted explicitly using a Connection Abort message (Byte 1 = 255) inside the TP.CM PGN (60416). What's usually left out is why an abort happens — the abort reason code, sent in byte 2, tells you exactly what went wrong:

Reason Code Meaning
1 Already in one or more connection-managed sessions and cannot support another
2 System resources were needed for another task, so this connection managed session was terminated
3 A timeout occurred and this is the connection abort to close the session
4 CTS received when data transfer is in progress
5 Maximum retransmit request limit reached
6 Unexpected data transfer packet
7 Bad sequence number (software cannot recover)
8 Duplicate sequence number (software cannot recover)
9 "Total Message Size" is greater than 1785 bytes
250 If a Connection Abort reason is unknown, this is the fallback value used
251-255 Reserved for SAE assignment / manufacturer-specific

Handling this correctly matters for two reasons: first, a receiver that ignores abort codes will keep buffering a session the sender has already killed, corrupting the next reassembly. Second, logging these codes is one of the most useful debugging tools available on a live J1939 network — an abort code of 7 or 8 almost always points to a sequence-numbering bug either in your stack or the peer's.

Sequence Numbers: Where Rollover Bugs Hide

Each Data Transfer (DT) packet carries a 1-byte sequence number, starting at 1. For a 1,785-byte message, the maximum packet count is 255 — which conveniently fits in one byte, so a well-formed message never actually needs to roll over 255. But two related bugs show up constantly in real implementations:

  • Off-by-one errors on the last packet. The final DT packet is often shorter than 7 data bytes, and the standard requires padding unused bytes with 0xFF. Implementations that forget this padding, or that miscalculate the final packet's expected length against the total byte count declared in RTS/BAM, will produce reassembled messages with garbage trailing bytes.
  • Sequence number 0 is invalid. DT sequence numbers start at 1, not 0. A stack that doesn't validate incoming sequence numbers can be tricked (or corrupted by noise) into accepting a malformed frame with sequence 0, silently corrupting the reassembly buffer instead of raising an abort.

Concurrent Session Handling

A node can be involved in multiple TP sessions at once — sending a BAM while also being the destination of an RTS/CTS session from a different node, for example. What the spec constrains is the same node acting as both sender and receiver of more than one connection-mode session on the same PGN/destination pair at the same time — that's not allowed, and a compliant receiver should reject a second RTS for the same PGN from the same source with an abort reason of 1 (already in one or more connection-managed sessions).

This is a real-world gotcha for gateway and telematics devices in particular, since they often sit in the middle of a lot of simultaneous traffic: if your stack doesn't track sessions per source-address/PGN pair, you'll see intermittent corruption whenever two multi-packet messages happen to overlap in time — which, on a busy commercial vehicle network, happens more often than test setups tend to reveal.

BAM Has No Flow Control — And That's a Design Trade-off, Not a Flaw

It's common to see BAM described as "the simpler, less reliable option," but the lack of flow control is intentional: BAM is meant for messages every node on the bus might care about (like DM1 with multiple active faults, or a broadcast VIN), and requiring per-receiver acknowledgment would be unworkable with an arbitrary number of listeners. The trade-off is that:

  • The sender paces itself at a fixed minimum interval between packets (commonly implemented around 50-200 ms, bounded by Th) regardless of how many receivers exist or how fast they can keep up.
  • There's no retry mechanism. If a receiver misses a DT packet in a BAM session (e.g., due to a bus error or buffer overrun), there's no CTS-style recovery — the receiver simply has an incomplete message and has to wait for the next full broadcast, if one comes.
  • A busy CAN bus (especially near the 2% network utilization ceiling many J1939 networks target for proprietary and best-effort traffic) increases the odds of a missed BAM packet, which is why safety- or diagnostic-critical multi-packet data is more often sent via connection mode when the destination is known.

A Realistic Failure Scenario

To tie the pieces together, here's a sequence that a byte-table alone won't show you:

  • Node A sends RTS to Node B for a 200-byte calibration payload, declaring it can send up to 3 packets per burst.
  • Node B responds with CTS, agreeing to receive 2 packets at a time starting from sequence 1.
  • Node A sends packets 1-2, then waits for the next CTS as agreed.
  • Node B is momentarily busy (servicing another interrupt) and doesn't send the next CTS within T2 (1250 ms).
  • Node A's T2 timer expires. A compliant sender aborts the session with reason code 3 (timeout) rather than retrying indefinitely.
  • Node B, unaware the sender gave up, is still expecting more DT packets — if it doesn't also implement T3, it will sit holding a half-reassembled buffer forever, silently leaking memory or blocking a future session for the same PGN.

Neither side did anything obviously "wrong" in isolation — the bug only shows up when both timer implementations are checked against each other, which is exactly the kind of interoperability issue that surfaces in the field rather than in unit tests against your own stack.

Practical Takeaways for Implementation

  • Implement all of T1-T4, not just the happy-path RTS→CTS→DT→ACK flow — timeout-driven abort is not optional for spec compliance.
  • Track TP sessions per source-address/PGN pair, not globally, so concurrent sessions from different nodes don't collide.
  • Validate sequence numbers strictly — reject 0, reject out-of-order packets, and abort with the correct reason code rather than silently dropping or overwriting buffer data.
  • Pad the final DT packet with 0xFF, and validate the reassembled length against the total byte count declared in RTS/BAM before handing the message to the application layer.
  • Log abort reason codes in the field — they're one of the most useful low-effort diagnostics available for intermittent bus issues.
  • This is also exactly the layer where a battle-tested protocol stack pays for itself: correct TP timer and session handling under real bus conditions — noise, concurrent sessions, momentarily busy nodes — is hard to fully validate in-house, and it's the layer most likely to produce intermittent, hard-to-reproduce field issues if implemented from scratch under time pressure.

FAQs

1,785 bytes. Anything larger is outside the standard TP mechanism's scope and would need a manufacturer-specific or higher-layer solution.

Most commonly either the receiver isn't responding to CTS within the sender's T1/T2 window, or the two implementations disagree on max-packets-per-burst — double-check that the CTS response never requests more packets than the RTS declared it could send at once.

No. BAM is a fire-and-forget broadcast — there's no CTS, no ACK, and no retry if a receiver misses a packet. This is a deliberate trade-off for messages meant for an arbitrary number of listeners.

Reason 7 (bad sequence number) and reason 8 (duplicate sequence number) almost always indicate a sequence-numbering bug — either dropped/reordered frames on a noisy bus, or an implementation error in how one side increments or validates the DT sequence counter.

Yes, as long as they don't involve the same source/destination/PGN combination simultaneously. A node should reject a second RTS for a session it's already running with an abort, rather than trying to interleave two reassembly buffers for the same PGN.