Home/Blog/OCPP 1.6 Architecture - How a Charger Talks to the Cloud
January 9, 2026
8 min read
3 views
🇺🇸 English

OCPP 1.6 Architecture - How a Charger Talks to the Cloud

A practical look at how OCPP 1.6 connects charge points to central systems using WebSockets, heartbeats, and status notifications.

OCPPEV ChargingProtocolWebSocketIoT
Published in Technology
OCPP 1.6 Architecture - How a Charger Talks to the Cloud

OCPP 1.6 Architecture. How a Charger Talks to the Cloud

Every EV charger you've ever plugged into does the same thing on boot: it opens a WebSocket to a server somewhere and says "I'm here." That's OCPP in a nutshell. The rest is details. but the details are where things break, so let's walk through them.


The two players

OCPP 1.6 is a client-server protocol. The Charge Point (the physical charger) is always the client. The Central System (your backend / CSMS) is the server. The charger connects outbound. never the other way around. This matters a lot for firewalls: a charger behind a NAT or 4G modem is fine because it initiates the connection.

mermaid
Rendering diagram...

The WebSocket lifecycle

OCPP 1.6J (the JSON variant. basically the only one anyone deploys now) runs over WebSocket. Here's the boot sequence, which you'll see hundreds of times in your logs:

  1. Charger opens a WebSocket to wss://your-backend.com/ocpp/CP001
  2. Charger sends BootNotification. vendor, model, serial, firmware version
  3. Backend responds Accepted (or Pending / Rejected) plus a heartbeat interval
  4. Charger starts heartbeating at that interval
  5. Both sides can now send messages freely
mermaid
Rendering diagram...

WebSocket is persistent and bidirectional. Unlike plain HTTP, the backend can push commands to the charger at any time. "start a charge," "unlock that connector," "go update your firmware." Without that push capability, fleet management doesn't work.


Message structure

Every OCPP 1.6J message is a JSON array. Three types:

Call (request):

json
[2, "uuid-123", "BootNotification", {"chargePointVendor": "Acme", "chargePointModel": "FC-50"}]

CallResult (response):

json
[3, "uuid-123", {"status": "Accepted", "currentTime": "2026-01-07T10:00:00Z", "interval": 300}]

CallError:

json
[4, "uuid-123", "InternalError", "Something went wrong", {}]

First element: 2 = request, 3 = response, 4 = error. The uuid ties a response to its request. That's the entire envelope. No XML namespaces, no SOAP headers. just a flat JSON array. This simplicity is a big part of why 1.6J won over 1.6S.


Heartbeats. more useful than they look

The charger sends a Heartbeat at regular intervals. The backend responds with the current time. Two things happen:

  1. The backend knows the charger is alive. If heartbeats stop, something is wrong. mark it offline, alert the operator.
  2. The charger synchronizes its clock. Many chargers don't have reliable NTP, so the backend's time becomes the source of truth for timestamps on transactions and meter values.

I've debugged billing discrepancies that ultimately traced back to chargers with drifted clocks because they'd lost their NTP server and the heartbeat interval was set too high. Set it to 300 seconds or less for production.


Status notifications and the connector state machine

Every time something changes on a connector. cable plugged in, charging starts, fault detected. the charger sends a StatusNotification.

mermaid
Rendering diagram...

This state machine is what drives the UI in charging apps. When someone opens an app and sees "Connector 1: Available". that came from a StatusNotification.


A complete boot-to-charge sequence

mermaid
Rendering diagram...

That's the entire lifecycle. Boot, heartbeat, status, authorize, start, meter, stop. Every OCPP 1.6 deployment in the world runs this loop, thousands of times a day.

Last updated: July 2, 2026

Get notified of new posts

No spam, unsubscribe anytime.