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.
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 |
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.
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.
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:
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.
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:
To tie the pieces together, here's a sequence that a byte-table alone won't show you:
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.
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.