Skip to main content
Work top-down when a call fails: device online → schema bound → permissions → correct URL → invoke. Set these for the copyable commands:

Quick diagnostic flow

Symptom index


Schema not bound

Symptoms:
  • GET …/rpc/services returns "services": [] with no descriptor_digest
  • Invoke returns 404 with "device descriptor not found"
Check:
  1. device-rpc module enabled — in modules.required and security.capabilities. See Omega setup.
  2. Device online — Console shows the device connected over MQTT.
  3. Device restarted after enabling RPC — the module publishes a schema announce on start and reconnect.
Fix:
Re-run list services. You should see a non-empty services array before invoking.

Device offline / can’t reach device

Symptoms:
  • Schema is bound but invoke returns 502, 503, or times out
  • Reflect succeeds but invoke never completes
  • Error text mentions the device cannot be reached
Check:
  1. Device online in Console — MQTT heartbeat recent.
  2. device-rpc running — Omega logs show the gRPC listener started (not memory transport only).
  3. MQTT credentials and TLS — cert not expired; broker reachable from the device network.
Fix:
  • Restart the device with MQTT transport and device-rpc enabled.
  • Reconnect MQTT (restart Omega or power-cycle) so the platform sees the device again.
  • Probe with a unary Echo call:
If list services works but invoke still fails, check Root topic mismatch.

Root topic mismatch

Symptoms:
  • Device shows online; schema bound; invoke still fails with reachability or bridge errors
  • Works with a local standalone echo server but not when registered in Golain
Cause: The cloud dials your device using the topic slug and device name from the Console. Omega connection.root_topic must match that assignment. Check: Confirm in Console device MQTT details or with golain omega scaffold. Fix: Update Omega client YAML or OMEGA_ROOT_TOPIC, redeploy, and verify list services still returns methods. Then retry invoke.

Permissions

Symptoms:
  • HTTP 403 with "message": "forbidden"
  • HTTP 401 with "message": "unauthorized"
Check:
  • Authorization: Bearer header present and token not expired
  • ORG-ID set to a valid organization UUID
  • Token is a user OIDC token — not a device MQTT credential
Fix: Grant can_invoke_rpc on the target device for your user or permission group. Reflect additionally requires can_write_device_metadata. Update permissions in Console, then retry.

Wrong URL

Symptoms:
  • 404 service not found in descriptor
  • 404 method not found in descriptor
  • Invoke hits the management API host instead of the RPC host
Fix:
  1. List services and copy exact services[].name and methods[].name:
  1. Use the full service name (for example golain.device.v1.EchoService), not a short alias.
  2. POST to the RPC host with no /core/api/v1 prefix:
Only unary methods use the sync path. Server-stream methods use /streams/{Service}/{Method}. See Invoking methods.

Streaming on wrong path

Symptoms:
  • HTTP 501 with "streaming rpc not supported" — you POSTed a server-stream, client-stream, or bidi method to the sync path (/{Service}/{Method})
  • HTTP 501 with "only server-streaming rpc supported" — you POSTed a unary or non-server-stream method to the /streams/ path
Fix: Example — move EchoServerStream from sync to streams:
For client-stream and bidi methods (EchoClientStream, EchoBidiStream), use device-local tooling — the cloud does not expose these shapes.

Stale schema after OTA

Symptoms:
  • New firmware deployed; invoke hits old methods or schema errors
  • List services shows an outdated method list
Check:
  1. Device restarted after OTA so it publishes a fresh schema announce.
  2. descriptor_digest from list services matches what the device logs after restart.
Fix:
Re-check list services. After breaking proto changes, expect a new digest — plan OTA rollouts so old and new firmware can coexist during migration.

Queued stuck or duplicates

Symptoms:
  • Poll shows pending or failed for a long time
  • Call becomes expired — device was offline past expires_at
  • Call shows succeeded but the device ran the handler twice
Check:
  1. Device online when the platform attempts delivery (queued calls retry on reconnect).
  2. expires_at not passed — default TTL is 900 s; extend with ?ttl_seconds= on enqueue (max 86400).
  3. attempts and last_error on the poll response.
Fix — poll status:
Bring the device online and wait for delivery. For long outages, enqueue again with a higher ttl_seconds. Fix — enqueue with longer TTL:
→ Full behavior: Queued invoke

Queued idempotency

Symptoms:
  • Motor started twice, counter incremented twice, or duplicate writes after one enqueue
  • succeeded in poll but duplicate real-world effects
Cause: Queued RPC is at-least-once. The platform may invoke your device more than once for the same logical call. Fix (device firmware):
  1. Add an idempotency key field to request protos; cache results per key on the device.
  2. Prefer naturally idempotent handlers where possible.
  3. Do not rely on call_id — it is not sent to the device today.
Omega setup — Idempotency

Self-hosted operators

If the device connects and publishes data but RPC schema never binds or invoke never reaches devices on a self-hosted stack, verify apis, mqtt-broker, and worker processes are running and topology is current. See Self-hosted deployment.

Operator checklist

Run in order for a single device:
Local dev: set API=https://localhost:19090/core/api/v1 and RPC_PROXY=http://localhost:8082.

Still stuck?

This guide covers Device RPC only. For legacy shell command RPC (exec:… over signed control topics), see Remote control — Legacy shell RPC.