Back to blog
Engineering9 min read

Debugging OCPP WebSocket Connections: 1006, Subprotocol Mismatches and TLS

A charger that will not connect gives you almost nothing to work with: a close code, maybe a log line. Here is how to read OCPP-J connection failures — 1006, subprotocol negotiation, TLS and the reconnect storms they cause.

I
Infinite Service Team

Every OCPP integration hits the same wall at least once. The charger is powered, the network is up, and the CSMS shows nothing. No BootNotification, no error, just silence — or a log line that says 1006 and stops.

OCPP-J runs over WebSocket, and almost everything that goes wrong before your first message goes wrong in the WebSocket handshake. This is a field guide to the failures we see most often.

Close code 1006 means "I have nothing to tell you"

1006 is the close code that most often shows up in charger logs, and it is the least informative one in the spec. It is defined as *abnormal closure*: the connection died without a WebSocket close frame ever being exchanged.

The important thing about 1006 is that it is never sent over the wire. It is a local invention — the WebSocket library on the charger generates it to describe the fact that the TCP connection vanished underneath it. So it tells you nothing about the CSMS's opinion, because the CSMS never got to express one.

That narrows the cause to the layers below OCPP:

  • The TCP connection was reset or timed out
  • TLS negotiation failed and the socket was torn down
  • An intermediary — load balancer, reverse proxy, mobile carrier NAT — dropped an idle connection
  • The HTTP upgrade request was rejected and the client closed before reading the response

If you are getting 1006 immediately on connect, it is the handshake. If you are getting it after minutes or hours of healthy traffic, it is almost always an idle timeout, and the fix is the heartbeat interval rather than anything in your code.

Subprotocol negotiation is the quiet killer

OCPP-J identifies its version through the WebSocket subprotocol header. The charger sends:

GET /ocpp/CP001 HTTP/1.1
Host: csms.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: ocpp1.6

The server must echo back exactly one of the offered protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: ocpp1.6

Three things go wrong here, and all three produce the same symptom — a connection that opens and instantly closes.

The server omits the header entirely. Many WebSocket libraries accept the upgrade happily and simply do not echo Sec-WebSocket-Protocol. A strict OCPP client treats the absence as a failed negotiation and closes. Your CSMS logs a successful connection; the charger logs a failure. Both are telling the truth.

The values do not match exactly. The subprotocol token is ocpp1.6, not ocpp1.6j, OCPP1.6, or ocpp-1.6. String comparison is case-sensitive and there is no normalisation step. A charger offering ocpp1.6 and a server answering OCPP1.6 have not agreed on anything.

The server picks a protocol that was not offered. If a charger offers only ocpp1.6 and the CSMS answers ocpp2.0.1 because that is its preferred version, the client is required to fail the connection.

When a charger offers several versions, they arrive in preference order:

Sec-WebSocket-Protocol: ocpp2.0.1, ocpp1.6

The server should pick the first one it supports, not the last one it parsed.

TLS problems that look like network problems

Production OCPP runs over wss://, and charger TLS stacks are frequently years behind the servers they talk to. The failures are unhelpfully generic.

The trust store is stale or missing. Embedded chargers ship with a CA bundle baked into firmware. If your CSMS certificate chains to a root that was added to the public trust stores after the charger's firmware was built, the charger cannot validate it. Nothing on the server side will show this — from the CSMS's perspective the client simply hung up during the handshake.

The intermediate chain is incomplete. Browsers paper over a missing intermediate certificate by fetching it themselves. Embedded TLS clients generally do not. A site that loads perfectly in Chrome can be unreachable to every charger in your fleet. Serve the full chain, not just the leaf.

SNI is missing. Older charger firmware may not send Server Name Indication. If your CSMS is behind a host that requires SNI to select a certificate, the connection gets the default certificate — which will not match, and the charger will reject it.

The protocol version is too old. A charger that only speaks TLS 1.0 or 1.1 cannot connect to a server that requires 1.2 or higher. This is the one failure where the right answer is usually to update the charger rather than weaken the server.

Reconnect storms

Once a fleet starts failing, the failure tends to amplify. A charger that cannot connect retries; if every charger retries on the same fixed interval, they synchronise, and the CSMS gets the entire fleet arriving in the same second, over and over.

Two things prevent this, and OCPP does not mandate either, so you have to build them:

  • Exponential backoff with a cap. Retry after 1s, 2s, 4s, 8s, up to a ceiling of a few minutes.
  • Jitter. Randomise each delay by ±20%. Without jitter, backoff still synchronises the fleet — it just synchronises it at longer intervals.

The same problem appears after a CSMS deployment. Every charger drops at once, and every charger reconnects at once. Backoff and jitter are what turn a thundering herd into a gentle ramp.

Reading the handshake directly

When logs disagree, look at the wire. A single curl tells you whether the server negotiates the subprotocol correctly:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Protocol: ocpp1.6" \
  https://csms.example.com/ocpp/CP001

You want 101 Switching Protocols and a Sec-WebSocket-Protocol: ocpp1.6 header in the response. If you get 101 without the header, you have found your bug — and it is in the CSMS, not the charger.

For the TLS layer, openssl s_client -connect csms.example.com:443 -showcerts prints the full chain the server is actually presenting, which is the fastest way to spot a missing intermediate.

Where a simulator helps

The hard part of all of this is that you are debugging two implementations at once and can only see one of them. A charge point simulator gives you a known-good client: if SimPilot connects to your CSMS and your charger does not, the bug is in the charger. If neither connects, it is the server.

Working in the other direction, TestPilot acts as a known-good CSMS, so you can point a physical charger at it and see whether the handshake and the first BootNotification are clean before your own backend is anywhere near the picture.

Either way the value is the same: replace one of the two unknowns with something you trust, and the remaining failure has only one place left to hide.

Related articles