Most solo IoT projects fail by copying the enterprise default: MQTT broker, cloud IoT suite, device twins, rules engine, dashboards, and certificates before the first paid user. My position is stricter: for a freelancer with no infrastructure budget, the popular default is usually the wrong first choice because it rents complexity before the product has earned it.
MQTT-first is usually a tax on a one-person project
MQTT is excellent technology, but MQTT-first IoT connectivity is often the wrong default for a solo freelancer because it introduces connection state, broker policy, retained messages, session expiry, and client lifecycle before you know whether anyone wants the device. MQTT 3.1.1 and MQTT 5.0 were designed for constrained messaging, but “designed for constrained messaging” does not mean “free to operate,” because somebody still has to debug reconnect storms, dead subscriptions, and stale retained values at midnight.
The guide IoT Connectivity and Device Management for Developers is worth keeping open for vocabulary, but its developer-centric checklist is too broad for a freelancer because each optional component becomes a support queue of one. I would not start with Eclipse Mosquitto 2.0, EMQX 5, HiveMQ, Kubernetes, Prometheus, Grafana, and a device-shadow model unless the project already has paying devices in the field, because every moving part increases the number of failure modes you alone must explain.
The common argument for MQTT is that it is lightweight, and that claim is true at the packet level because a PUBLISH packet can be tiny compared with HTTP headers. The problem is that your risk is not packet size first; your risk is unpaid operations, because a single freelancer loses more time to certificate rotation, broker persistence, and client retries than to a few extra bytes in an HTTP request.
MQTT QoS 1 is also oversold for early products because “at least once” delivery means duplicates are allowed, so you still need idempotency keys on the server. QoS 2 sounds safer, but it costs a four-step handshake, so it is hard to justify before you can prove that duplicate handling is more expensive than extra latency. MQTT retained messages are convenient, but they become confusing when a device receives yesterday’s command after a reinstall, because the broker cannot know whether stale state is safe for your hardware.
A practical first build can use HTTP/1.1 or HTTP/2, TLS 1.3 from RFC 8446, a bearer token, and a single ingest table. That sounds less “IoT-native,” and that is the point: boring web infrastructure is easier to rent cheaply because every host, CDN, log drain, and uptime checker understands it already. CoAP from RFC 7252 can be a good later option for constrained devices, but I would not choose it first unless the device truly cannot afford HTTP, because CoAP debugging tools are less familiar to most clients who will hire a freelancer.
An HTTP inbox beats a device shadow until state is your real product
The device shadow, device twin, or desired/reported-state pattern is the most seductive default in IoT device management because it gives the comforting feeling that every device has a canonical cloud object. I think that is backward for most freelance projects because the first version usually needs to answer one question: “What did this device last report?” rather than “Can a distributed state machine reconcile desired and reported properties under intermittent connectivity?”
Start with an inbox. A device posts readings, firmware version, battery voltage, RSSI, and a monotonic boot counter. The server stores the raw event and updates a small “last seen” row. That design is crude, but it is debuggable because every action leaves an append-only trail. JSON is acceptable at the start because humans can read it during field failures, while CBOR from RFC 8949 can wait until bandwidth costs are proven painful.
This small Flask 3.x service is not a full platform, but it runs and shows the shape I would use before introducing a broker:
from flask import Flask, request, abort
import sqlite3, time
app = Flask(__name__)
db = "devices.sqlite"
with sqlite3.connect(db) as c:
c.execute("create table if not exists reading(device text, ts int, body text)")
@app.post("/ingest/<device>")
def ingest(device):
if request.headers.get("Authorization") != "Bearer dev-token":
abort(401)
with sqlite3.connect(db) as c:
c.execute("insert into reading values (?,?,?)", (device, int(time.time()), request.get_data(as_text=True)))
return {"stored": True}
app.run(host="0.0.0.0", port=8080)
This snippet should sit behind Caddy 2.8 or nginx 1.26 for TLS, because public plaintext device traffic leaks credentials and payloads. SQLite is not fashionable for IoT, but it is defensible for a solo prototype because one file gives you backups, inspection, and local reproduction without running PostgreSQL, Redis, and a message broker. If the device count grows, you can move the same append-only event shape to PostgreSQL 16, TimescaleDB 2.15, or ClickHouse 24 without changing the device protocol first.
For numbers, use them as design brakes rather than decoration. In a small local measurement on an M2 laptop, a Flask-plus-SQLite endpoint can persist 1,000 tiny JSON posts in a few seconds, which is enough to test product behavior long before it proves production capacity. I would tune the heartbeat timeout to 900 seconds for a device that reports every 5 minutes, because three missed intervals are easier to explain than a noisy alert on the first delayed packet. AWS lists IoT Core messaging in many regions around a per-million-message model, often quoted as $1 per million 5 KB messages in US East, which is cheap per packet but not free once you add rules, logs, and debugging time. Render’s public pricing has historically put a starter web service around $7 per month, which is predictable enough for a freelancer who mainly needs one HTTPS endpoint and logs.
Managed IoT suites win later, but they charge attention on day one
The explicit comparison is simple: AWS IoT Core wins when you need managed MQTT, X.509 client certificates, fleet indexing, rules, shadows, and integration with AWS Lambda or Amazon Timestream; it costs cloud learning, IAM policy work such as iot:Connect and iot:Publish, plus metered messages and logs. A tiny HTTPS app on Render, Fly.io, Railway, or a small VPS wins when you have fewer devices, irregular revenue, and no dedicated operator; it costs you the loss of built-in IoT features, but it saves enough attention to finish the product.
That trade is not anti-cloud, because managed cloud is excellent once the problem is stable. It is anti-premature-cloud-specialization, because IoT suites make architecture feel settled before the product’s failure modes are known. Azure IoT Hub, AWS IoT Core, Google Cloud Pub/Sub, ThingsBoard 3.7, and Losant all solve real problems, but adopting them early can hide the basic business question because dashboards and device registries look like progress.
The companion post IoT Connectivity and Device Management for Developers gives useful nouns for the field, but I would still postpone most of them because naming a fleet is easier than operating one. Device registries are useful when you have devices to revoke, groups to roll out, and customer-specific policy. Before that, a table with device_id, token_hash, firmware_version, last_seen_at, and disabled_at is usually enough because it supports authentication, inventory, and a kill switch.
MQTT can still be the right choice when devices must receive commands instantly, stay connected behind NAT, or publish many small messages per minute, because long-lived connections reduce repeated handshakes. HTTP wins when devices wake rarely, send a status report, and sleep, because each request is independent and easier to retry safely. WebSocket from RFC 6455 sits between them, but I would not use it for sleepy devices because holding a socket open wastes power and server memory when the device has nothing to say.
Be honest about battery and radio behavior. On ESP32-class hardware using ESP-IDF 5.2, Wi-Fi association and TLS negotiation can dominate energy use, so batching readings often saves more than switching JSON to CBOR. A value I would tune deliberately is MQTT keepalive=300 seconds rather than copying keepalive=60, because longer intervals can reduce chatter for tolerant applications while still detecting dead connections. For MQTT 5.0, clean_start=false and session_expiry_interval are powerful, but they require explicit cleanup rules because forgotten sessions can queue messages for devices that will never return.
Device management should start with replacement, revocation, and rollback
Many IoT plans start with fleet dashboards, but a freelancer should start with the three events that create angry emails: a device is lost, a credential leaks, or a firmware update breaks behavior. That is device management at the useful level because it protects the client before it produces attractive charts.
Use per-device credentials from the beginning, even if they are only random 128-bit tokens stored as hashes, because shared secrets turn one leaked unit into a fleet compromise. X.509 certificates are stronger for mutual TLS, but they add manufacturing and renewal work, so I would not require them on the first paid pilot unless the client’s risk demands hardware-bound identity. If you do use certificates, document the certificate authority, validity period, and revocation path, because “we use certs” is not a plan when a device is sold, stolen, or returned.
For firmware, pick the simplest update path that can roll back. ESP-IDF OTA partitions, Mender 3.6, RAUC, SWUpdate, and Zephyr 3.7’s MCUboot all provide real mechanisms, but each one needs a release process. The metric I would track first is OTA success rate over the last 20 updates, because a percentage over a small recent window catches current breakage better than a lifetime vanity number. Another practical threshold is “disable rollout after 2 consecutive boot failures,” because a device that repeatedly reboots is telling you the update is unsafe.
I would not deploy Kubernetes for the backend of a solo IoT product, because the operational surface of Ingress, cert-manager, persistent volumes, cluster upgrades, and container observability is larger than the first product needs. Docker Compose with one app container, Caddy, and a volume backup is less impressive, but it is easier to restore at 2 a.m. because the entire system fits in one mental model. Prometheus 2.52 and Grafana 11 are excellent once there is traffic to observe, but an uptime check, structured JSON logs, and a daily SQLite backup usually produce more value at the start because they answer the failures you can act on.
Security should be narrow and concrete. Use TLS 1.3, reject missing tokens, hash stored secrets with Argon2id or at least a strong keyed HMAC, and include a server-side disabled_at check on every ingest. JSON Schema 2020-12 can validate payloads, because malformed data is cheaper to reject at the edge than to clean later. OpenTelemetry is useful for tracing distributed systems, but I would skip it in the first version because a one-process service does not need distributed tracing to explain a failed POST.
Start by deleting one component from the architecture sketch
Your first concrete move should be to draw the system, cross out the broker, and build one authenticated HTTPS ingest path with a device table and a revocation flag. Add MQTT, shadows, fleet indexing, and dashboards only when a specific failure proves they are cheaper than the simpler design. For a solo freelancer, saved attention is budget.


