TutorialsTutorials

Open RAN Tutorial: From Architecture to Your First xApp

A complete hands-on path into Open RAN: the architecture and interfaces that matter, an acronym decoder, standing up a lab with open-source components, writing an xApp against E2SM-KPM, and adding A1 policy.

By Manas·19 min read·Updated 2026-08-25

Open RAN has an unusually steep entry ramp — not because the concepts are hard, but because the acronym density is punishing and most available material is either a vendor pitch or a specification written for people who already know the answer.

This tutorial takes the practical route: understand the minimum architecture, decode the vocabulary, get a lab running, and write something that actually talks to a RAN. For conceptual grounding, What is Open RAN? covers the why and O-RAN architecture explained is the reference.

Prerequisites. You should know the 5G RAN protocol stack (RRC, PDCP, RLC, MAC, PHY), what the CU/DU split is, and roughly what a scheduler does. Comfort with Linux, containers, and Python or C++ will make the lab portion far smoother. You do not need prior O-RAN exposure.


Step 1 — The architecture you actually need

Ignore the full reference diagram initially. Four things matter to start.

The minimum Open RAN mental model: SMO and Non-RT RIC on top, Near-RT RIC in the middle, and O-CU, O-DU and O-RU below.

O-CU, O-DU, O-RU — the disaggregated base station. O-CU holds RRC/SDAP/PDCP, O-DU holds RLC/MAC/upper PHY, O-RU holds lower PHY and RF. The O-DU/O-RU boundary uses Split 7-2x, O-RAN's contribution; the CU/DU split is 3GPP's.

Near-RT RIC — hosts xApps, controls the RAN over E2, operating between 10 ms and 1 second.

Non-RT RIC — lives in the SMO, hosts rApps, works above 1 second, sends policy to the Near-RT RIC over A1.

SMO — Service Management and Orchestration. Reaches network functions over O1 and cloud infrastructure over O2.

The rule of thumb for which controller does what: if the decision needs history and analysis, it's an rApp; if it needs current RAN state, it's an xApp; if it needs to happen within 10 ms, it stays in the O-DU and neither RIC is involved.

That last clause matters more than it sounds. A common early misconception is that the Near-RT RIC can influence per-slot scheduling. It cannot — a control loop crossing a network interface can't meet slot timing. Anything sub-10 ms is the O-DU's business permanently.


Step 2 — Decode the vocabulary

The acronyms are the real barrier. Here's the working set, roughly in the order you'll meet them.

TermExpansionWhat it actually is
SMOService Management and OrchestrationThe management layer above everything
RICRAN Intelligent ControllerThe control platform; two flavours
Non-RT RICNon-real-time RICPolicy and analytics, > 1 s, inside the SMO
Near-RT RICNear-real-time RICRAN control, 10 ms – 1 s
xAppApplication on the Near-RT RIC
rAppApplication on the Non-RT RIC
E2Near-RT RIC ↔ RAN interface
E2APE2 Application ProtocolThe E2 procedures
E2SME2 Service ModelWhat a given E2 node exposes
E2 nodeAn O-CU or O-DU speaking E2
RAN functionOne E2SM instance inside an E2 node
A1Non-RT RIC ↔ Near-RT RIC policy interface
O1SMO ↔ network functions, FCAPS
O2SMO ↔ O-Cloud infrastructure
O-CloudThe cloud platform hosting O-RAN functions
Open FronthaulO-DU ↔ O-RU, four planes
Split 7-2xThe functional split inside the PHY
WG1–WG11Working GroupsWho writes which specification

Two disambiguations that save confusion later:

"RIC" alone is ambiguous. People say it meaning either controller. Ask which.

"E2 node" is not a piece of hardware. It's a logical role — an O-CU or O-DU that has registered with the RIC over E2 and declared what it can expose.


Step 3 — Understand the interfaces you'll touch

Five interfaces, but you'll spend nearly all your time on one.

InterfaceConnectsYou'll use it for
E2Near-RT RIC ↔ O-CU/O-DUEverything in xApp development
A1Non-RT RIC ↔ Near-RT RICSending policy to your xApp
O1SMO ↔ network functionsFCAPS, NETCONF/YANG
O2SMO ↔ O-CloudInfrastructure lifecycle
Open FronthaulO-DU ↔ O-RURadio integration, C/U/S/M planes

E2 in detail

E2 has two layers, and the distinction confuses newcomers more than anything else in Open RAN.

E2AP (application protocol) is the transport — procedures for setting up connections, subscribing, and exchanging messages. Generic, carried over SCTP.

E2SM (service model) defines what is exposed. This is the actual contract between xApp and RAN.

E2 structure: E2AP procedures carrying service-model-specific content between the Near-RT RIC and E2 nodes.

The service models

E2SM-KPM (Key Performance Measurement) — read-only metrics. Throughput, PRB utilisation, connected UEs, per-UE statistics. Start here. Read-only means you can't break anything, and it has the broadest implementation support.

E2SM-RC (RAN Control) — actually change RAN behaviour. Handover control, radio resource allocation, bearer configuration, QoS adjustment.

E2SM-CCC (Cell Configuration and Control) — cell-level configuration and state.

E2SM-NI (Network Interface) — exposes messages on other RAN interfaces.

The critical design point: an xApp is written against a service model, not against a vendor's RAN. That's the entire portability promise. In practice, service model coverage varies between implementations — check what your RAN actually supports before designing around a feature.

The E2AP procedures

Four you'll use constantly:

E2 Setup — the E2 node connects to the RIC and declares its supported RAN functions, each corresponding to a service model with a version. This is where you find out what the RAN can actually do.

RIC Subscription — your xApp requests specific data on specific triggers. The subscription contains an event trigger definition (when to report) and one or more action definitions (what to report).

RIC Indication — the RAN sends data matching your subscription. Contains an indication header (metadata) and indication message (the payload).

RIC Control — you tell the RAN to do something. Used with E2SM-RC, not KPM.

Two more worth knowing: RIC Service Update, where a node changes its advertised functions mid-session, and E2 Node Configuration Update, for cell configuration changes.

E2SM-KPM structure

Since this is where you'll start, it's worth knowing what a KPM subscription actually contains.

Event trigger definition — usually a reporting period in milliseconds.

Action definition — which measurements, at what granularity. KPM defines report styles:

StyleGranularityTypical use
1E2 node levelCell-wide metrics
2Single UEOne specific UE
3UEs matching a conditionFiltered subsets
4All UEs matching a conditionPer-UE across a group
5A specified UE listNamed UEs

Measurement names are defined in 3GPP TS 28.552 — DRB.UEThpDl, RRU.PrbTotDl, RRC.ConnMean and so on. Using the correct 3GPP measurement name matters; a typo produces a subscription that's accepted but never fires.

Start with style 1 and a one-second period. It's the least likely to overwhelm you and the most likely to be supported.


Step 4 — Stand up a lab

Three broad options, in increasing order of realism and effort.

Three lab approaches: RIC with simulated E2 nodes, RIC with a software RAN and UE simulator, and RIC with software RAN and SDR hardware.

Option A — RIC plus simulated E2 nodes

Lightest. Run a Near-RT RIC and connect a simulated E2 node generating plausible metrics.

Good for learning E2AP procedures, developing xApp logic, and iterating fast. No real radio behaviour, so anything depending on actual channel conditions is meaningless.

Start here regardless of where you're heading. Debugging an xApp against a simulator is vastly easier than against a live RAN, and most early bugs are in subscription handling rather than your logic.

Practical requirements are modest: a Linux machine with Docker or Kubernetes, 8–16 GB RAM, and patience with container orchestration.

Option B — RIC plus software RAN plus UE simulator

A software RAN stack with E2 support, connected to a simulated UE, connected to your RIC.

Realistic protocol behaviour and real scheduling decisions without RF hardware. The sweet spot for most learning and a large share of actual xApp development.

Expect integration to be the hard part. Version compatibility between RAN, RIC, and service model implementations is where the time goes. Budget more setup time than you think.

Requirements step up: 16–32 GB RAM, multiple cores, and ideally a machine you can dedicate to it.

Option C — RIC plus software RAN plus SDR

Add software-defined radio hardware and a real device or UE simulator over the air.

Genuinely realistic and considerably more work — RF configuration, timing, synchronisation, and a set of failure modes that don't exist in simulation. Worth it for research depending on real channel behaviour.

You'll need SDR hardware, appropriate RF front ends, and in most jurisdictions either shielded enclosures or a test licence. Don't transmit on licensed spectrum without authorisation — this is a legal matter, not a best practice.

The open-source landscape

O-RAN Software Community (OSC) — the O-RAN Alliance's own reference implementations, including a Near-RT RIC platform and Non-RT RIC components. The most specification-faithful starting point, and correspondingly heavier to run.

srsRAN Project — open-source 5G RAN with E2 support. Widely used in research and reasonably approachable.

OpenAirInterface (OAI) — full 5G stack including RAN and core, large research community. Powerful, steeper learning curve.

FlexRIC — lightweight RIC and E2 agent implementation. Popular precisely because it's much easier to get running than a full RIC platform, and a good first target.

Open5GS — open-source 5G Core if you need one behind your RAN.

ns-3 with O-RAN modules — simulation-based, useful for large-scale scenarios where real-time behaviour isn't required.

Check current documentation for versions before committing to a combination. This ecosystem moves quickly and version mismatch is the single most common reason a lab won't come up.

A sane setup order

  1. Get the RIC platform running on its own. Confirm it's healthy before adding anything.
  2. Connect a simulated E2 node. Confirm E2 Setup completes and the RIC lists the node's RAN functions.
  3. Deploy a sample or hello-world xApp. Confirm it registers.
  4. Only then write your own.

Skipping to step 4 means debugging your code, the platform, and the connection simultaneously.


Step 5 — Write your first xApp

The first useful xApp does one thing: subscribe to KPM metrics and act on what arrives.

The lifecycle

Every xApp follows the same shape:

Register with the RIC platform, declaring what it needs.

Discover available E2 nodes and their RAN functions.

Subscribe over E2, specifying the target node, service model, metrics, and trigger.

Receive indications as the RAN sends matching data.

Process and decide — your actual logic.

Optionally control — send a RIC Control message via E2SM-RC.

The xApp lifecycle from registration through subscription and indication handling to optional control actions.

The xApp descriptor

Before code, most RIC platforms want a descriptor — JSON declaring the xApp's name, version, container image, resource requirements, the messages it sends and receives, and any configuration parameters.

This is how the platform knows how to deploy you and how to route messages. Getting the message types wrong here produces an xApp that deploys cleanly and never receives anything — a frustrating failure mode because nothing errors.

A monitoring xApp

# 1. Register with the RIC platform
xapp = XAppFramework(name="kpm-monitor", config="config.json")

# 2. Discover E2 nodes and check what they expose
nodes = xapp.get_e2_nodes()
for node in nodes:
    functions = node.ran_functions
    kpm = next((f for f in functions if f.model == "E2SM-KPM"), None)
    if kpm:
        log.info(f"{node.id} supports KPM v{kpm.version}")

# 3. Subscribe: style 1, cell-level, one-second period
subscription = xapp.subscribe(
    e2_node_id    = "gnb_001",
    ran_function  = kpm.id,
    event_trigger = PeriodicTrigger(period_ms=1000),
    action        = KpmAction(
        report_style = 1,
        measurements = ["DRB.UEThpDl", "RRU.PrbTotDl", "RRC.ConnMean"],
    ),
)

# 4. Handle each indication
@xapp.on_indication(subscription)
def handle(indication):
    header  = decode_kpm_header(indication.header)
    metrics = decode_kpm_message(indication.message)

    thp  = metrics.get("DRB.UEThpDl", 0)
    prb  = metrics.get("RRU.PrbTotDl", 0)
    ues  = metrics.get("RRC.ConnMean", 0)

    log.info(f"[{header.timestamp}] thp={thp} prb={prb} ues={ues}")

    # Congestion heuristic: high PRB usage, low delivered throughput
    if prb > 0.8 and thp < THROUGHPUT_FLOOR:
        log.warning(f"Cell {header.cell_id} congested — spectral efficiency low")
        # A control xApp would issue a RIC Control message here

xapp.run()

The real thing has more ceremony — ASN.1 codecs, platform SDK specifics, health endpoints, graceful shutdown — but the logical shape is exactly this.

Adding a control action

Moving from monitoring to control means E2SM-RC:

# Only after the monitoring version works reliably
if should_rebalance(metrics):
    xapp.control(
        e2_node_id   = "gnb_001",
        ran_function = rc_function.id,
        control_action = HandoverControl(
            ue_id       = target_ue,
            target_cell = candidate_cell,
        ),
    )

Treat this step with respect. A control xApp can degrade a live network, and unlike a monitoring bug the damage isn't confined to your logs.

Things that will catch you out

ASN.1 encoding. E2 messages are ASN.1 PER-encoded. Use the SDK's codec. Hand-rolling this is a poor use of time and a rich source of subtle bugs.

Service model version mismatch. Your xApp targets one E2SM-KPM version, the RAN implements another. Symptoms are confusing: subscriptions accepted but no indications arriving, or decodes that produce garbage. Check versions on both sides first — this is the most common failure in early xApp work.

Wrong measurement names. Using a name not in TS 28.552, or one your RAN doesn't implement, produces an accepted subscription that never fires. Log the RAN's supported measurement list at startup.

Subscription granularity. Requesting per-UE metrics across hundreds of UEs at 10 ms periodicity floods you. Start coarse — cell level, one second — and tighten only when you need to.

Timing expectations. The Near-RT RIC operates at 10 ms to 1 second. Logic assuming per-slot influence won't work architecturally.

Subscription cleanup. An xApp that crashes without deleting its subscriptions leaves them active on the E2 node. Restart enough times and you'll exhaust subscription capacity. Handle shutdown properly.

Message routing. If your xApp deploys but receives nothing, check the descriptor's message-type declarations before debugging your code.

Common errors

SymptomLikely cause
E2 Setup never completesSCTP connectivity, or node not configured with RIC address
RAN function list emptyE2 agent not enabled in the RAN build
Subscription rejectedService model version or unsupported report style
Subscription accepted, no indicationsWrong measurement names, or trigger never fires
Indications arrive, decode failsE2SM version mismatch
xApp deploys, receives nothingDescriptor message types wrong
Subscription capacity exhaustedPrevious instances didn't clean up

Step 6 — Add A1 policy

The canonical Open RAN pattern is two-tier: an rApp learns over long timescales and sends policy; an xApp reacts in near-real-time within that policy.

A1 policy is a typed JSON document. The Non-RT RIC creates a policy instance of a given policy type; the Near-RT RIC delivers it to xApps that registered interest.

A traffic-steering example: the rApp analyses weeks of load data and produces a policy saying prefer offloading UEs from cell A to cell B when A exceeds 70% PRB utilisation, subject to a minimum RSRP on B. The xApp receives that and makes the actual per-UE decisions minute to minute based on live measurements.

Neither could do the other's job. The rApp has no real-time visibility; the xApp has no historical data.

Implementing this means: define or adopt a policy type schema, have your xApp register for that type, handle policy create/update/delete callbacks, and — importantly — decide what your xApp does when no policy is present. A sensible default is required, because policy delivery isn't guaranteed.


Step 7 — Where to go next

Open Fronthaul — if you're working with radios, WG4's specifications and the M-plane NETCONF/YANG model are the next area. This is where multi-vendor integration effort actually concentrates.

O1 and O2 — NETCONF/YANG for management, and O-Cloud infrastructure interfaces. Less glamorous, closer to what operations teams deal with daily.

Specifications, selectively. O-RAN publishes a great deal. Worth having open: the architecture description for orientation, WG3's Near-RT RIC and E2 specifications for xApp work, WG4 for fronthaul, and WG2 for A1 and the Non-RT RIC.

PlugFest reports. They're where the gap between "specification compliant" and "actually interoperates" becomes visible. The published findings are consistently more informative than vendor material.

AI in the loop. Once a monitoring xApp works, the natural extension is a learned policy rather than a threshold. See AI-RAN explained for where that fits and what it costs.


A realistic first-project sequence

  1. Get a Near-RT RIC running on its own and confirm it's healthy.
  2. Connect a simulated E2 node; confirm E2 Setup and RAN function discovery.
  3. Deploy a sample xApp; confirm registration.
  4. Write your own xApp subscribing to E2SM-KPM style 1; log metrics.
  5. Add simple logic — detect a condition, log a decision.
  6. Replace the simulator with a software RAN and UE simulator.
  7. Verify your xApp still works against real protocol behaviour.
  8. Add a bounded control action via E2SM-RC, tested against the simulator first.
  9. Add an rApp sending A1 policy that shapes your xApp's behaviour.

Each step is a working system. Resist skipping to step 8 — the debugging surface there is enormous and the ecosystem's error messages are unhelpful.


Reference

  • O-RAN ALLIANCE — architecture description and working group specifications
  • O-RAN WG2 — Non-RT RIC and A1 interface
  • O-RAN WG3 — Near-RT RIC architecture, E2AP and E2 service models
  • O-RAN WG4 — Open Fronthaul control, user, synchronisation and management planes
  • O-RAN Software Community — reference implementations
  • 3GPP TS 28.552 — Management and performance measurements for 5G, the KPM measurement names
  • What is Open RAN? — the rationale and deployment reality
  • O-RAN architecture explained — the interface and RIC reference
TutorialsOpen RANO-RAN