> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Operational Endpoints

> Probe liveness, readiness, license status, and Prometheus metrics on the broker's dedicated HTTP port

## Check the broker without opening MQTT

Orchestrators and monitoring systems need a plain HTTP answer: is the process up, is it accepting clients, and what is it doing. The Coreflux MQTT Broker serves that on a **dedicated ops port** (default `9100`), separate from MQTT and from the WebSocket data plane.

> [!TIP]
>
> **Like a side door for the building inspector.** Devices still use the front door (MQTT). Kubernetes and Prometheus use the side door, a small HTTP port that only answers health and status questions.

Use these endpoints when you need to:

* tell a container orchestrator when to restart the process, and when to send it traffic;
* scrape broker gauges and counters into Prometheus;
* read version, ports, and license status from the broker host without an MQTT login.

The listener is **unauthenticated**. Keep port `9100` on a private network. See [Security](#security).

***

These paths answer only on the ops port. The same path on the WebSocket / HTTP port (default `5000`) returns **404**.

## Endpoints

| Method | Path | Success | What you get |
| - | - | - | - |
| `GET` | `/health`, `/healthz` | `200` | Liveness. Always healthy while the process is serving. |
| `GET` | `/ready`, `/readyz` | `200` or `503` | Readiness. `503` until startup has finished **and** MQTT is accepting connections. |
| `GET` | `/info` | `200` | Build, runtime, ports, and short summaries. No secrets. |
| `GET` | `/license` | `200` | License tier, limits, usage, fingerprint, activation, and enrollment. |
| `GET` | `/metrics` | `200` | Prometheus text. |
| `GET` | `/cluster` | `200` | High-availability summary. Clustering off is still `200`. |
| `GET` | `/loglevel` | `200` | Current minimum log level. |
| `POST` | `/loglevel` | `200` or `400` | Change the minimum log level until the next restart. |

All JSON responses use camelCase property names.

## Liveness

`GET /health` (alias `/healthz`) means the process is up and answering HTTP. It does **not** mean MQTT is accepting clients. Use `/ready` for that.

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "status": "healthy",
  "version": "2.14.0",
  "timestamp": "2026-09-28T10:00:00Z"
}
```

| Field | Meaning |
| - | - |
| `status` | Always `healthy` on this path. |
| `version` | Broker version string. |
| `timestamp` | UTC time of the response (`yyyy-MM-ddTHH:mm:ssZ`). |

## Readiness

`GET /ready` (alias `/readyz`) returns `200` only when **both** are true:

1. The broker application has finished starting.
2. The MQTT server is accepting connections.

Until then the status is **503**.

Ready:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "status": "ready",
  "timestamp": "2026-09-28T10:00:00Z"
}
```

Not ready:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "status": "not_ready",
  "reason": "MQTT server is not accepting connections yet",
  "timestamp": "2026-09-28T10:00:00Z"
}
```

| `reason` | When |
| - | - |
| `application is still starting` | The process is up, but startup has not finished. |
| `MQTT server is not accepting connections yet` | Startup finished, and the MQTT listener is not up yet. |

Point a Kubernetes **liveness** probe at `/health` and a **readiness** probe at `/ready`. A liveness probe on `/ready` restarts the broker during a normal startup.

## Runtime info

`GET /info` is a single JSON document: name, version, uptime, operating system, Python sandbox, cluster and MCP summaries, telemetry status, listener ports, and the CPU, memory, and GC view the process actually has. It does not include passwords, tokens, license files, or the cluster shared secret.

A standalone broker (clustering and MCP off, census not yet pinged) has this shape. `runtimeResources` below is trimmed to the CPU and GC fields operators compare first; the live document also includes process memory, the thread pool, container signals, and cgroup data.

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "name": "Coreflux DataHub",
  "version": "2.14.0",
  "uptimeSeconds": 1234.5,
  "timestamp": "2026-09-28T10:00:00Z",
  "os": {
    "type": "Linux",
    "version": "Debian GNU/Linux 13",
    "architecture": "X64",
    "framework": ".NET 10.0"
  },
  "python": {
    "available": true,
    "mode": "coreflux-runner",
    "version": "3.12.0",
    "backend": "linux-native",
    "isolationLevel": "OsNamespaces"
  },
  "cluster": { "enabled": false },
  "mcp": { "enabled": false },
  "telemetry": {
    "enabled": true,
    "reason": null,
    "lastPingAt": null,
    "lastStatus": null
  },
  "ports": {
    "mqtt": 1883,
    "mqttTls": 8883,
    "websocket": 5000,
    "websocketTls": 443,
    "ops": 9100
  },
  "runtimeResources": {
    "runtime": {
      "processorCount": 2,
      "is64BitProcess": true,
      "enableWriteXorExecute": true
    },
    "gc": {
      "isServerGc": true,
      "configuration": { "ServerGC": true }
    }
  }
}
```

`name` is the broker configuration name (`Coreflux MQTT Broker` if that name is unset). `ports` reflects the listeners in effect, including `ops`. `uptimeSeconds` is `0` when the process start time cannot be read.

### Python sandbox

| `available` | `mode` | `backend` | `isolationLevel` | Meaning |
| - | - | - | - | - |
| `false` | `null` | `null` | `null` | Python execution is unavailable. |
| `true` | `coreflux-runner` | Reported by the runner, or `unknown` | Reported by the runner, or `unknown` | Scripts run in the sandboxed runner. `unknown` means the runner has not answered yet. |
| `true` | `in-process` | `python.net` | `none` | Legacy in-process Python, with no sandbox boundary. |

### Cluster and MCP summaries

`cluster` is `{ "enabled": false }` when high-availability clustering is off. When it is on, the summary is `enabled`, `clusterId`, `node`, `role` (lowercase), `isLeader`, `leader`, and `members` (the member count). While a configured cluster fails to start, it is `enabled: false` with `configured: true` and a `startFailure` block. Full status is [`GET /cluster`](#cluster-status).

`mcp` is `{ "enabled": false }` when the [MCP Server](/latest/mqtt-broker/mcp-server) is off. When it is on, the summary is:

| Field | Meaning |
| - | - |
| `enabled` | `true` |
| `endpoint` | Full URL, such as `http://127.0.0.1:6278/mcp` |
| `bind` | Bind address |
| `port` | MCP listen port |
| `toolsCount` | Tools currently advertised |
| `tokensActive` | How many tokens exist — never the token values or the usernames they map to |
| `auth` | `bearer` |

That object is not the MCP listener. MCP `/health` and MCP `/info` stay on the MCP port (default `6278`).

### Telemetry summary

`telemetry` is the anonymous install-census status. It does not include an install id, hostname, or license.

| Field | Meaning |
| - | - |
| `enabled` | `true` when the daily census is allowed to run. |
| `reason` | Why it is off: `starting` before the check finishes, `COREFLUX_TELEMETRY`, `DO_NOT_TRACK`, or `Telemetry.Enabled`. `null` when it is on. |
| `lastPingAt` | UTC time of the last attempt, or `null` before the first one. |
| `lastStatus` | HTTP status from the platform, or `timeout` / `error`. `null` before the first attempt. |

Set `COREFLUX_TELEMETRY` to `off`, `false`, or `0` to turn the census off. `COREFLUX_TELEMETRY` affects only the census. `DO_NOT_TRACK=1` turns off the census and also turns off [fleet performance metrics](/latest/mqtt-broker/licensing#fleet-performance-metrics). `Telemetry.Enabled: false` in the broker configuration turns the census off too. The environment is checked first.

### Runtime resources

`runtimeResources` is the live view of CPU, memory, garbage collection, the thread pool, and (on Linux) cgroup limits. The fields operators compare most often:

| Field | Meaning |
| - | - |
| `gc.configuration.ServerGC` | Server GC as **requested**. A `DOTNET_gcServer` or `COMPlus_gcServer` value wins; otherwise this is the mode baked in at publish. |
| `gc.isServerGc` | The mode **actually in effect**. |
| `runtime.processorCount` | Processors the runtime can see. |
| `runtime.enableWriteXorExecute` | Effective write-xor-execute mode for generated code. `false` on `linux-arm` / ARMv7 publishes, `true` on other targets, unless `DOTNET_EnableWriteXorExecute` or `COMPlus_EnableWriteXorExecute` overrides it. |
| `cgroup` | Linux cgroup files the process could read (CPU quota, memory max, cpuset). Empty off Linux. Keys look like `cpu.max (/sys/fs/cgroup/cpu.max)`. |

`ServerGC` and `isServerGc` disagree when the runtime silently drops to workstation GC. That happens when only one processor is visible: a one-CPU container, a cgroup quota of one core, or a Kubernetes `limits.cpu` of `1` or less (`500m` is one processor). Give the broker at least two CPUs to keep Server GC, and confirm with `processorCount` and `cgroup`.

The same object also includes process memory (`process`), thread-pool counts (`threadPool`), container signals (`container`), and `dotNetEnv` — only `DOTNET_GC*`, `DOTNET_ThreadPool*`, `COMPlus_*`, `DOTNET_EnableWriteXorExecute`, and `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT`. Broker passwords and tokens are not in this document.

## License status

`GET /license` returns the license status: tier, limits, usage, fingerprint, activation, and enrollment. A broker with no license still answers, with `tier` `Community`.

HTTP property names are **camelCase**. The retained MQTT topic `$SYS/Coreflux/License` carries the same status with PascalCase names (`Tier`, `EffectiveLimits`), except `isTrial`, which is lowercase on both. Field meanings are on [Licensing and Enrollment](/latest/mqtt-broker/licensing#watch-license-status).

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "licensedTo": "N/A",
  "expiresUtc": null,
  "secondsToExpiry": null,
  "isActive": false,
  "tier": "Community",
  "isTrial": false,
  "fingerprint": "…",
  "fingerprintSource": "machine-id",
  "activation": {
    "mode": "offline",
    "bindingOk": true,
    "tamperDetected": false
  },
  "enrollment": {
    "pending": false,
    "reason": null,
    "enrolled": false,
    "boundOrg": null
  },
  "effectiveLimits": {
    "maxArtifacts": 24,
    "maxRoutes": 3,
    "maxConnections": 10000
  },
  "currentUsage": {
    "artifactCount": 4,
    "routes": 1
  }
}
```

`effectiveLimits` also includes `maxModels`, `maxRules`, `maxActions`, `maxVisus`, and `maxCertInTls`. `currentUsage` also includes `actions` (regular actions, route events, Python scripts), `models`, `rules`, and `panels`. Connector counts still use the name `routes`.

If the status cannot be read, the response is **500**:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "error": "failed to read license status",
  "detail": "…"
}
```

> [!WARNING]
>
> `GET /license` is unauthenticated. It exposes the hardware fingerprint, plan, and enrollment state. Do not publish port `9100` on the public internet. Bind it to `127.0.0.1` when only local probes need it. MQTT clients that already authenticate can read the same status on `$SYS/Coreflux/License`.

## Metrics

`GET /metrics` returns Prometheus text exposition format, version 0.0.4 (`text/plain; version=0.0.4; charset=utf-8`).

A scrape still succeeds if the live client list cannot be read. In that case `coreflux_clients_connected`, `coreflux_sessions_total`, and `coreflux_retained_messages_total` are `0`.

| Metric | Type | Meaning |
| - | - | - |
| `coreflux_up` | gauge | `1` while the process is running. |
| `coreflux_ready` | gauge | `1` when the broker is ready to accept MQTT traffic (same rule as `/ready`). |
| `coreflux_uptime_seconds` | gauge | Process uptime, in seconds. |
| `coreflux_clients_connected` | gauge | MQTT clients connected right now. |
| `coreflux_sessions_total` | gauge | Active MQTT sessions. |
| `coreflux_retained_messages_total` | gauge | Retained messages held. |
| `coreflux_messages_published_total` | counter | Application messages clients published to the broker since process start. |
| `coreflux_messages_delivered_total` | counter | Application messages the broker delivered to clients since process start. |
| `coreflux_subscriptions_total` | counter | `$SYS` topic subscriptions since the daily stats reset. Data-topic subscriptions are not counted. |
| `coreflux_bytes_sent_total` | counter | Bytes sent since process start. |
| `coreflux_bytes_received_total` | counter | Bytes received since process start. |
| `coreflux_cpu_usage_ratio` | gauge | Process CPU over the last second, as a fraction of one (0–1). When a cgroup CPU quota is set, the fraction is of that quota; otherwise it is of all cores. `NaN` for the first second after start. |
| `coreflux_auth_failures_total` | counter | CONNECTs the broker refused, for any reason (bad credentials, a reserved client id, connect rules, or session expiry). |
| `coreflux_acl_denied_total` | counter | Publishes and subscriptions refused by the ACL. The `op` label is `publish` or `subscribe`. |
| `coreflux_rate_limited_total` | counter | Publishes refused by the per-client rate limit. |
| `coreflux_route_dropped_total` | counter | Messages shed by a saturated route channel. |
| `coreflux_errors_total` | counter | Failures in routes, LoT actions, LoT models, and Python scripts. The `component` label is `route`, `action`, `model`, or `python`. |
| `coreflux_process_memory_bytes` | gauge | Process working set, in bytes. |
| `coreflux_gc_heap_bytes` | gauge | Managed heap size, in bytes. |

> [!NOTE]
>
> Counters other than `coreflux_subscriptions_total` count from process start and return to zero only when the broker restarts, so Prometheus `rate()` and `increase()` work across midnight. In earlier releases, `coreflux_messages_published_total`, `coreflux_bytes_sent_total`, and `coreflux_bytes_received_total` followed the daily stats file (`FluxCapacitorStats_*.json`) and dropped to zero at UTC midnight. That daily file still keeps its per-day totals. `coreflux_subscriptions_total` still counts `$SYS` subscriptions since the daily stats reset.
>
> The same counters feed an enrolled broker's [fleet performance metrics](/latest/mqtt-broker/licensing#fleet-performance-metrics). They keep updating whether or not fleet metrics are turned on.

### When durability or clustering is active

Each family below always includes an `*_enabled` gauge (`0` or `1`). The rest of the family is emitted only while that feature is active.

| Family | When you see more than the enabled gauge | Where to read the feature |
| - | - | - |
| `coreflux_durability_enabled`, `coreflux_wal_*`, `coreflux_durable_*` | [MQTT state durability](/latest/mqtt-broker/durability) is active (WAL appends, fsyncs, durable retained messages, sessions, and queued QoS 1/2). | Durability |
| `coreflux_cluster_*`, `coreflux_partitions_*`, `coreflux_partition_*`, `coreflux_replication_*` | [High-availability clustering](/latest/mqtt-broker/clustering) is on. Includes leadership, member counts, Raft progress, partition ownership, quorum replication, rejected publishes, delivery to other nodes, and failover. | Clustering |

## Cluster status

`GET /cluster` is the [high-availability clustering](/latest/mqtt-broker/clustering) (beta) view of one node. It never includes the cluster shared secret. Ask the leader for the authoritative view of the other nodes: only the leader checks whether members are alive, so a follower reports other nodes as `unknown`.

The answer has one of three shapes.

**Clustering off**

Clustering is disabled, not granted by the license, its configuration is invalid (the log names the problem), or the node is still starting:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "enabled": false,
  "message": "Clustering is not enabled on this node.",
  "timestamp": "2026-10-04T10:00:00Z"
}
```

**Start failing**

Clustering is configured but could not start, for example because `AdvertisedHost` does not resolve, the cluster port is in use, or the data directory is not writable. Until it starts, the node serves its clients but is not part of the cluster. It retries after 5 s, then with doubling delays up to one minute; a rising `attempts` means the cause persists:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "enabled": false,
  "configured": true,
  "message": "Clustering is configured but failed to start; this node is not part of the cluster. Retrying.",
  "startFailure": {
    "error": "Cluster: could not resolve the advertised host 'broker-a' within 30s; set Cluster.AdvertisedHost to an address peers can reach.",
    "attempts": 3,
    "firstFailedAt": "2026-10-04T10:00:00Z",
    "lastFailedAt": "2026-10-04T10:01:10Z",
    "nextAttemptAt": "2026-10-04T10:01:50Z"
  },
  "timestamp": "2026-10-04T10:01:20Z"
}
```

**Running**

A healthy member of a 3-node cluster (trimmed):

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "enabled": true,
  "identity": { "clusterId": "factory-floor", "node": "broker-a", "endpoint": "10.0.0.11:9200" },
  "leadership": { "role": "leader", "isLeader": true, "leader": "broker-a" },
  "raft": { "term": 7, "commitIndex": 25, "lastAppliedIndex": 25, "hasState": true, "standby": false, "standbyRecoveries": 0, "logReset": null, "quorumSize": 2 },
  "members": [
    { "name": "broker-a", "endpoint": "10.0.0.11:9200", "isSelf": true, "isLeader": true, "status": "alive" },
    { "name": "broker-b", "endpoint": "10.0.0.12:9200", "isSelf": false, "isLeader": false, "status": "alive" },
    { "name": "broker-c", "endpoint": "10.0.0.13:9200", "isSelf": false, "isLeader": false, "status": "alive" }
  ],
  "membersTotal": 3,
  "membersAlive": 3,
  "partitions": { "enabled": true, "mapVersion": 14, "partitionCount": 32, "replicationFactor": 3, "underReplicated": 0 },
  "replication": { "enabled": true, "quorumGateEnabled": true, "connectedPeers": 2, "quorumFailures": 0, "lastQuorumMicros": 2446 },
  "timestamp": "2026-10-04T10:05:00Z"
}
```

When clustering is running, the document has these blocks:

| Block | What it shows | What to look for |
| - | - | - |
| `identity` | `clusterId`, this `node`, and the `endpoint` the other nodes use to reach it | `endpoint` matches how the other nodes list this node in `members` |
| `leadership` | `role` (`leader`, `follower`, or `candidate` while no leader is known), `isLeader`, and the `leader`'s name | Every node names the same leader |
| `raft` | Election `term`, the `commitIndex` and `lastAppliedIndex` of the cluster's own bookkeeping (never MQTT messages), `quorumSize`, `hasState` (this node belongs to a cluster), `standby` and `standbyRecoveries`, and `logReset` | `term` rising quickly means repeated elections. `logReset` is set when the node found its cluster log unreadable at start, moved it aside, and rejoined from the leader |
| `discovery`, `topology` | Discovery mode and the QoS 0 topology | — |
| `members`, `membersTotal`, `membersAlive` | Each member's `name`, `endpoint`, `isSelf`, `isLeader`, and `status` | `membersTotal` equals your number of nodes; on the leader, `membersAlive` equals `membersTotal` |
| `dataPlane` | Names of the links that carry QoS 0 messages to the other nodes | One per other node with the `mesh` topology |
| `partitions` | `mapVersion`, `partitionCount`, `replicationFactor`, `underReplicated`, the number of partitions each node owns, this node's partitions, and the full map (`owner`, `replicas`, and `epoch` per partition). `enabled: false` until the cluster has formed | Owners spread evenly across nodes; the map version rises after each node failure or return |
| `replication` | `quorumGateEnabled`, `connectedPeers`, and counters since `startedUtc`: accepted and forwarded publishes, rejected publishes (`quorumFailures`), the last time to reach a majority (`lastQuorumMicros`), and the `wal`, `forwarding`, `sessions`, `failover`, `consistency`, and `transport` blocks. `partitionProgress` lists, for each partition this node holds, the last stored sequence and epoch | `connectedPeers` equals the number of other nodes; `forwarding.backlog` near `0`; `forwarding.dropped` stays `0`. The same `partitionProgress` values on every copy of a partition mean the copies agree |
| `lot` | The committed LoT version and the version this node has applied, each node's state and why it is behind, `mixedMode` while a live node can't take part in LoT changes, and who owns each timer, `ON START` action, and exclusive connector | Every node has applied the committed version; `mixedMode` is `false` |
| `routeListeners` | Each server connector's listener on this node (Modbus server, OPC UA server, REST API server, video output) and its state, with the reason when a bind failed | Every listener is up on every node |

The counters restart at `0` when the node restarts. The short form on [`/info`](#runtime-info) is enough for a dashboard tile. See [Clustering](/latest/mqtt-broker/clustering#monitoring) for the values to watch.

## Log level

`GET /loglevel` and `POST /loglevel` change the broker's minimum log level without a restart. The new level lasts until the process exits. The next start uses `COREFLUX_LOG_LEVEL` again (default `Information`).

Read the current level:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -s http://127.0.0.1:9100/loglevel
```

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{ "level": "Information" }
```

Set it with a query string or a JSON body. If both are sent, the query string wins. Names are case-insensitive; the response uses the canonical name.

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -s -X POST 'http://127.0.0.1:9100/loglevel?level=Debug'
```

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -s -X POST http://127.0.0.1:9100/loglevel \
  -H 'Content-Type: application/json' \
  -d '{"level":"Debug"}'
```

Success is `{ "level": "Debug" }`. An unknown or missing level is **400**:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "error": "invalid or missing level",
  "allowed": ["Verbose", "Debug", "Information", "Warning", "Error", "Fatal"]
}
```

> [!WARNING]
>
> `POST /loglevel` is unauthenticated. Anyone who can reach the ops port can raise verbosity for the whole process. Keep the port on a private network.

## Configuration

The ops listener is on by default. Environment variables override the broker configuration. Restart the broker after changing them.

| Broker configuration | Environment variable | Default | Effect |
| - | - | - | - |
| `OpsEnabled` | `COREFLUX_OPS_ENABLED` | `true` | Turn the whole ops listener on or off. |
| `OpsPort` | `COREFLUX_OPS_PORT` | `9100` | TCP port. Must be from `1` to `65535`. |
| `OpsBindIP` | `COREFLUX_OPS_BIND_IP` | `0.0.0.0` | Bind address on the broker host. `127.0.0.1` keeps probes on the broker host only. |

When `COREFLUX_OPS_ENABLED` is set, the listener is on only for `1` or `true` (any letter case). `false`, `0`, and any other value turn it off. When the variable is unset, `OpsEnabled` is used.

An invalid `COREFLUX_OPS_PORT` (not an integer, or outside `1`–`65535`) is ignored, and `OpsPort` is used instead. A non-empty `COREFLUX_OPS_BIND_IP` replaces `OpsBindIP`.

When the listener is disabled, nothing is bound to the ops port and these paths are not served.

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
export COREFLUX_OPS_ENABLED=true
export COREFLUX_OPS_PORT=9100
export COREFLUX_OPS_BIND_IP=127.0.0.1
```

The same names are listed under [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env#operational-http-health-ready-info-license-metrics).

## Security

These endpoints are unauthenticated on purpose, so orchestrator probes and in-cluster scrapers do not need a password.

* Do not expose port `9100` to the public internet.
* On a single broker host, bind `127.0.0.1` (or publish the Docker port on `127.0.0.1` only).
* In Kubernetes, leave the port on the pod network and scrape it with a Service. Do not put it on a public LoadBalancer.
* Treat `/license` and `POST /loglevel` as sensitive even though `/health` is not.

`/info` is intentionally free of secrets: no MQTT passwords, no MCP token values, no license file, no cluster shared secret.

## Kubernetes

The published broker image has no shell, so use HTTP probes rather than a command inside the container.

```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ports:
  - name: mqtt
    containerPort: 1883
  - name: ops
    containerPort: 9100
livenessProbe:
  httpGet:
    path: /health
    port: 9100
  initialDelaySeconds: 5
  periodSeconds: 15
readinessProbe:
  httpGet:
    path: /ready
    port: 9100
  initialDelaySeconds: 5
  periodSeconds: 10
```

A longer startup example is on [Startup Options](/latest/mqtt-broker/startup-options).

Prometheus Operator scrape (the Service must expose a port named `ops`):

```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: coreflux-broker
spec:
  selector:
    matchLabels:
      app: coreflux-broker
  endpoints:
    - port: ops
      path: /metrics
      interval: 30s
```

## Try it on the broker host

With the default port published on the broker host:

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -s http://127.0.0.1:9100/health
curl -s http://127.0.0.1:9100/ready
curl -s http://127.0.0.1:9100/info
curl -s http://127.0.0.1:9100/license
curl -s http://127.0.0.1:9100/metrics
```

Confirm the ops paths are not served on the WebSocket / HTTP port (expect `404`):

```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/health
```

## Next Steps

> **[System topics](/latest/mqtt-broker/sys-topics)**
>
> Read the same license status, and the rest of the broker, over MQTT.

> **[Licensing and Enrollment](/latest/mqtt-broker/licensing)**
>
> Interpret tier, limits, fingerprint, activation, and enrollment.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.