DRAFT — not indexed by search engines. Visible only via direct URL or the admin page.

OCPI Pull vs Push: When to Use Each Pattern

OCPI supports both pull and push patterns per module. Here is how each works, which modules use which, and the tradeoffs that matter in production.

OCPI uses two main interaction patterns: pull (the consumer asks for data) and push (the producer sends data when it’s ready). Different modules use different patterns, and getting the choice right per module matters for both correctness and performance.

This article explains the two patterns, which OCPI modules use which, and the operational tradeoffs. If you are new to the protocol, start with what OCPI is for the role and module basics.

The two patterns

Pull (request-response): the consumer initiates the conversation. Sends an HTTP GET to the producer’s endpoint. Gets a response with the current state.

[ Consumer ] -- GET /locations --> [ Producer ]
[ Consumer ] <-- locations data --- [ Producer ]

Pull is what you’d call a normal REST API pattern. Stateless, idempotent (in the HTTP sense), and predictable.

Push (server-initiated): the producer sends data to the consumer’s endpoint when something happens. The consumer must have a URL the producer can POST/PUT to.

[ Producer ] -- PUT /sessions/{id} --> [ Consumer ]
[ Producer ] <-- 200 OK ----------------- [ Consumer ]

Push is event-driven. The producer doesn’t wait for the consumer to ask; it sends the update right away.

OCPI uses both patterns, depending on the module and the use case.

Module-by-module pattern guide

A reference for which OCPI modules use which pattern.

Locations: pull-and-cache + push updates

Initial sync: the consumer pulls (GETs) the producer’s Locations once. Returns all current Locations. Possibly paginated.

Updates: the producer pushes individual Location updates as they happen. PUT to the consumer’s Locations endpoint with the changed Location.

In practice, many consumers do a periodic re-pull (daily or weekly) as a safety net against missed pushes. Locations data doesn’t change frequently so the redundancy is cheap.

Sessions: push primarily

The producer (CPO) pushes Session updates to the consumer (eMSP) as the session progresses.

A typical session generates 3-10 pushes:

  • Session START (when the user begins charging).
  • One or more interim updates (state changes, periodic energy updates).
  • Session END.

Pull is also supported (the consumer can GET a specific session), but the push pattern is the primary delivery mechanism.

CDRs: push only

CDRs are the immutable billing records. The producer (CPO) pushes the CDR to the consumer (eMSP) once the session is complete and the CDR is final.

CDRs use push with retry. The producer owns delivery — if the push fails, it retries until it succeeds (or until a defined max time). The consumer must acknowledge with 200 OK.

There’s no pull pattern for CDRs in the standard sense. CDRs aren’t a queryable collection; they’re delivered events.

Tariffs: pull primarily

The producer publishes its tariffs. The consumer pulls them. The producer can also push tariff updates when they change.

In practice, tariffs change infrequently and pull-on-demand or periodic pull is common.

Tokens: query-on-demand (pull) + push for whitelisting

When a CPO needs to authorize a user identifier, it queries the eMSP via Token query (a synchronous GET or a real-time query).

Separately, the eMSP can push its whitelist (or pieces of it) to CPOs that support local authorization caching.

The pattern is mixed: real-time pulls for authorization, optional push for caching.

Commands: push (asynchronous)

A consumer (eMSP) sends a Command request to the producer (CPO). The CPO acknowledges the request, processes asynchronously, then pushes a Command response back when done.

The asynchronous nature is because commands like RemoteStart have a real-world delay — the charger needs to receive the command, react, and respond. The whole flow can take seconds to a minute.

HubClientInfo: pull-and-cache + optional push

Standard pull pattern. The hub publishes its client list; consumers pull. Optional push for subscribers who want real-time updates.

Credentials: synchronous request-response

The Credentials handshake is a pure pull/POST pattern. PUT and POST messages exchanged at registration time. No background push.

ChargingProfiles: push from eMSP to CPO

The eMSP pushes a ChargingProfile to the CPO for a specific session. The CPO acknowledges and applies it.

Why this mix

Each module’s pattern was chosen with its specific needs in mind.

Locations is mostly static data with occasional updates. Pull-and-cache is efficient. Push updates the cache without full re-pull.

Sessions are events. Push is the natural pattern — the producer knows when the event happened; the consumer wants to know immediately.

CDRs are billing-critical. Push ensures delivery; retry ensures durability. Pull would risk loss if the consumer’s polling missed a window.

Tariffs are reference data. Pull is fine; updates are infrequent.

Tokens are mixed-frequency. Real-time queries need pull (synchronous). Whitelisting needs push (push the list once, query offline).

Commands are user-initiated actions. Async push handles the real-world delay.

Choosing patterns module by module gives you the right efficiency/correctness tradeoff for each data type. The cost is that an OCPI implementation must support multiple patterns rather than one.

Push design: doing it right

If you’re implementing the receiving side of a push pattern (Sessions, CDRs, ChargingProfiles, etc.), several things matter.

Idempotency. The sender will retry on perceived failures. The same message may arrive twice. Deduplicate by the natural key (session_id for Sessions, cdr_id for CDRs).

Acknowledge fast. Return 200 OK quickly. If you have heavy processing (database writes, downstream notifications), do them async after returning.

Return meaningful errors. If you can’t process the push (validation failure, your system is down for the partner), return an OCPI StatusCode that tells the sender what to do. Permanent failure → don’t retry. Transient failure → retry later.

Authentication. Verify the bearer token on every incoming push. Don’t trust the source URL alone. See OCPI security and authentication (coming soon) for how tokens are exchanged and rotated.

Rate limiting. A misbehaving partner might push aggressively. Protect yourself with per-partner rate limits.

Logging. Log every received push for audit. Include enough detail to reconstruct what happened if a partner disputes.

Push design: doing it right (sender side)

If you’re implementing the sending side (a CPO pushing Sessions/CDRs to eMSPs):

Retry on failure. Push isn’t fire-and-forget. If the receiver returns 5xx, retry with exponential backoff. If 4xx (bad request), log and stop retrying — the receiver said “no.”

Queue and batch. Don’t fail your own system if a partner’s endpoint is down. Queue messages locally and drain when they’re back.

Per-partner retry policies. Some partners are reliable; some are flaky. Tune backoff per partner.

Deduplication keys. Generate unique IDs (session_id, cdr_id) that survive retries cleanly.

Eventual consistency. Accept that some pushes arrive late. Design downstream systems to handle late arrivals (a CDR that arrives 6 hours late is still a valid CDR).

Observability. Track per-partner push success rates, latency, retry counts. Spot problems early.

Pull design: doing it right (consumer side)

If you’re pulling from a partner’s endpoint:

Cache aggressively. Pulled data is often stable (Locations, Tariffs). Don’t re-pull on every operation.

Respect pagination. Always follow the Link header for next pages. Don’t assume the first page is everything.

Background refresh. Schedule pulls on a sensible interval (daily or weekly for Locations, more frequent for Tariffs). Don’t pull on demand for hot-path operations.

Handle 304 Not Modified. If the partner supports it, conditional GETs save bandwidth on unchanged data.

Failure tolerance. A failed pull shouldn’t break your application. Use last-known-good data and alert on staleness.

Pull design: doing it right (producer side)

If you’re serving GET endpoints:

Pagination is essential. Large result sets break the world. Default page sizes of 100-1000 are reasonable.

Filtering and search. Support date-range filters for time-series data (Sessions, CDRs).

ETags / Last-Modified. Support conditional GET so consumers can skip unchanged data.

Performance. Locations and Tariffs GETs may run frequently. Index appropriately.

Rate limiting. Even pulls can be abusive. Protect against runaway consumers.

Mixed patterns in practice

Real-world OCPI implementations often blend the patterns.

Example: a CPO running new chargers comes online.

  1. The CPO’s CSMS publishes new Locations.
  2. It pushes Location PUTs to all connected eMSPs (push for the new data).
  3. Some eMSPs also do a periodic re-pull (pull for safety net).
  4. When users start sessions, the CPO pushes Session updates (push).
  5. eMSP cross-references Tokens via pull (real-time query).
  6. Session completes; CPO pushes CDR (push with retry).
  7. eMSP pulls Tariffs occasionally to keep its rate cache fresh (pull-and-cache).
sequenceDiagram
    participant CPO
    participant eMSP
    CPO->>eMSP: PUT Locations (push new)
    eMSP-->>CPO: GET Locations (re-pull safety net)
    CPO->>eMSP: PUT Session updates (push)
    eMSP->>CPO: GET Token (real-time query)
    CPO->>eMSP: POST CDR (push with retry)
    eMSP-->>CPO: GET Tariffs (pull and cache)

A single connection between two parties involves all patterns.

Common pitfalls

Treating push as fire-and-forget. Without acknowledgment-based retry, push doesn’t guarantee delivery. Always wait for 200 OK and handle failures.

Polling when you should push. “I’ll just poll the Sessions endpoint every minute” misses fine-grained updates and burns bandwidth. Implement push instead.

Push without idempotency. Receiver gets duplicates; data is corrupted. Always idempotent.

Ignoring pagination on pull. First page is not the whole story.

Caching pulled data forever. Tariffs change; Locations get added; your cache goes stale. Define cache-invalidation rules.

The honest summary

OCPI’s pull/push pattern mix is intentional. Each module has data characteristics that justify its pattern choice. Implementations have to handle both. The biggest mistakes are treating push as fire-and-forget, ignoring idempotency, and polling when push is the better choice. Many of these show up repeatedly in real integrations — see common OCPI integration pitfalls (coming soon) for the field-tested list. Get the patterns right per module and your OCPI integration will be both performant and correct.

Quick check

Q1. Which pattern does OCPI use for Sessions as the primary delivery mechanism?
Q2. On the sender side, what should you do when a receiver returns a 4xx error to a push?
Q3. Why does OCPI use pull-and-cache for Locations rather than push-only?
Q4. What makes Commands an asynchronous push flow?

Frequently asked questions

Why does OCPI support both patterns instead of just one?

Different modules have different data freshness needs. Locations rarely change so pull-and-cache works well. Sessions update frequently so push notifications make sense. CDRs are anchor billing records so push (with retry) ensures delivery. The protocol chose flexibility over uniformity.

Can I implement just pull or just push for everything?

For modules where the spec defines push, you should implement push or you violate the spec. For modules where pull is the only option (or the default), implement pull. Most real implementations end up doing both in different places.

Which is more reliable?

Push when implemented well with proper retry logic. The sender owns delivery and knows when the message is acknowledged. Pull depends on the polling frequency — fresh data may sit unread for the polling interval.

What is the biggest mistake with push patterns?

Not implementing idempotency on the receiving end. The sender retries on failure; the receiver may get the same message twice. Without idempotency, this causes duplicates (especially painful for CDRs).

Found this useful? Share it.