A fleet can keep publishing telemetry while its service system quietly loses track of which physical device produced it. I would treat that identity gap—not radio throughput—as the first scaling failure in a 5G IoT deployment, because reconnects, replacements, and offline repairs multiply faster than a backend developer expects. More capacity cannot fix an event attached to the wrong asset.
Device identity breaks before message throughput does
Wireless Infrastructure and Embedded Software for 5G IoT puts the radio and firmware in the same frame, but a backend also needs a durable answer to a less visible question: which installed asset sent this event? A modem’s network address is a poor answer because a new 5G PDU session can change its network context. A SIM identifier is insufficient because a technician can move a SIM without moving the sensor. The firmware’s device identifier, the modem identifier, and the service system’s asset identifier should therefore be separate fields connected by time-bounded assignments.
Consider a controller replaced during a site visit. The old controller may upload buffered readings after the replacement is registered, while the new one begins reporting immediately. If the ingestion service looks up only the asset’s current controller, late readings from the old unit can appear to come from the new one. Store an assignment’s start and end times, retain the controller identifier on every event, and resolve the asset using the event’s asserted capture time alongside its server receipt time. Keep both times because an offline device clock can be wrong; neither timestamp alone proves when a measurement occurred.
Reconnects create a second identity problem: duplicates. MQTT 5.0 QoS 1 provides at-least-once delivery, so a publisher that does not receive an acknowledgment may resend a message the server already committed. Give each event a device ID, a boot ID, and a sequence number that increases within that boot. Generate the boot ID once at startup and persist it before publishing; where reliable boot IDs are unavailable, use a persisted monotonic boot counter instead. A random message ID generated on every retry defeats deduplication because the same reading then looks new.
This PostgreSQL 15 example makes repeated inserts with the same identity harmless. It is an ingestion boundary, not a complete asset registry:
CREATE TABLE ingest_events (
device_id text NOT NULL,
boot_id uuid NOT NULL,
seq bigint NOT NULL CHECK (seq >= 0),
payload jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (device_id, boot_id, seq)
);
INSERT INTO ingest_events (device_id, boot_id, seq, payload)
VALUES ('node-7', '00000000-0000-4000-8000-000000000001', 42, '{"temp_c":21.4}')
ON CONFLICT (device_id, boot_id, seq) DO NOTHING;
Do not treat DO NOTHING as proof that two payloads match: it also hides a firmware defect that reuses a sequence number for different data. Compare a SHA-256 digest on conflict and quarantine mismatches, because silently accepting the first payload makes later diagnosis nearly impossible. Keep the raw controller ID and assignment version with the accepted event so a corrected asset mapping can be applied without inventing data.
Reconnect bursts defeat steady-state capacity estimates
Average event rate is the wrong first load test because an outage synchronizes devices that normally transmit independently. As a planning example rather than an observed fleet measurement, imagine 12,000 devices each buffering one reading per minute during a 30-minute interruption: recovery presents 360,000 readings before counting live traffic. The calculation is simple; the arrival pattern is not, because devices may reconnect together when coverage or a shared gateway returns.
I would cap replay separately from live publishing, because a backlog that consumes every worker can make current device state look stale even after the network recovers. Give each device a replay rate and a maximum in-flight count, then apply jitter to its reconnect delay. MQTT 5.0 Receive Maximum can bound unacknowledged QoS messages within a connection, but it cannot enforce a fleet-wide database write budget; the ingestion service still needs queue-depth limits and backpressure. If devices cannot retain data indefinitely, specify which readings they may discard and send a gap marker, because an unexplained hole is harder to interpret than an explicit loss.
Measure the path in stages rather than timing only the API handler. Record the interval from device capture to broker receipt, broker receipt to durable write, and durable write to asset association. Prometheus histograms can expose those distributions, while OpenTelemetry trace IDs can connect a sampled receipt to its database write. Prometheus’s documented default global scrape interval is 1 minute when it is not configured; shorten or lengthen scrape_interval for this deployment based on the duration of the bursts you need to see. A one-minute scrape cannot describe a reconnect surge that rises and clears between samples.
Set an initial replay-lag alert at 90 seconds as a value to tune, not a claim about acceptable latency everywhere. Pair it with the age of the oldest queued event and the count of devices still replaying, because a healthy average can conceal a small group that never catches up. In a replay test, report the measured 99th-percentile capture-to-association delay rather than substituting HTTP response time; the former includes the failure the operator actually sees. Run that test with forced disconnects and controller replacements, not only a traffic generator, because neither a synthetic message stream nor a steady connection exercises the identity transition.
Field repairs turn stale commands into the dangerous workload
Digital Infrastructure and IoT Field Service Software is a useful pairing of systems, but the troublesome boundary between them is command ownership. A field technician may replace a controller while an automation service has a configuration change queued for the old one. If the command is addressed only to the asset, the replacement can receive an instruction intended for different hardware. Address commands to a specific controller assignment and reject them after that assignment ends.
A command needs an expiry, an expected configuration revision, and an idempotency key because offline delivery can occur long after approval and retries can outlive the original request. An operator-selected expiry of 45 minutes might suit a nonurgent configuration change; it should be shorter for an action whose meaning changes quickly. Store the command’s requested state separately from its delivery and application states. “Broker accepted” is not “device applied,” and a device acknowledgment is not proof that a physical repair succeeded.
Use a monotonically increasing assignment generation as a fencing token. When a technician registers a replacement, advance the generation in one database transaction; require every subsequent command acknowledgment to include the generation under which it was issued. A late acknowledgment from the old controller can then be retained for audit without changing the new controller’s state. This is the same principle used to fence stale workers in distributed systems, applied to equipment that may disappear from the network for hours.
I would not let a field-service app edit device-to-asset mappings directly in a cached document store, because an offline edit and an automated replacement can otherwise overwrite each other without a detectable ordering. Have the app submit an assignment-change request with an expected generation, then let the authoritative service accept or reject it. The app can still work offline by holding requests locally in SQLite with WAL mode and showing them as pending; it must not present an unsynchronized replacement as confirmed. JSON Schema draft 2020-12 can validate the request shape, but an expected-generation check must happen against current server state because a schema cannot settle a race.
A transactional outbox beats a new event platform until its limits are measured
For a backend team entering this area, I would start with a PostgreSQL transactional outbox rather than make Kafka 3.7 the first destination for every device event, because the first hard requirement is keeping an accepted event and its assignment decision consistent. Insert the event and an outbox row in one transaction; a worker publishes the outbox row to downstream consumers and retries until acknowledged. Consumers still need idempotency because a worker can publish successfully and crash before recording completion.
PostgreSQL outbox versus Kafka-first ingestion is a choice about failure boundaries, not a contest over which tool sounds more scalable. The outbox wins when asset assignments and accepted events already live in PostgreSQL and the team needs one transactional decision; it costs database storage, polling or change-data-capture work, and careful cleanup of delivered rows. Kafka-first ingestion wins when several independent consumers need sustained replay and the team can operate partitions, retention, and consumer lag; it costs another durable system and does not, by itself, make a Kafka append atomic with a PostgreSQL assignment update. Kafka’s partition ordering also applies within a partition, so choose a stable device key if per-device order matters.
NATS JetStream is another credible durable transport, but replacing Kafka with JetStream does not remove the transaction boundary because broker acknowledgment and an asset-registry commit remain separate operations. Redis 7 can help with short-lived rate counters, but I would not make an expiring Redis key the sole record of a controller replacement because losing that key would change the interpretation of historical events. Choose transport after replay tests identify a bottleneck; otherwise the team may buy throughput while leaving the wrong-device problem intact.
The first scale test should replace a controller mid-replay
Start with one test fixture: disconnect a device, buffer readings, register a replacement controller, then reconnect the old one while the new one publishes. Assert that every reading retains its original controller ID, resolves against the correct assignment generation, and survives duplicate delivery. Record capture-to-association lag during the test. If that sequence fails, adding ingestion capacity will only make incorrect asset histories arrive faster.


