Every GPS tracking project reaches this fork, usually about an hour in. Both protocols carry the same forty bytes of latitude and longitude. Both are supported here, through the same normaliser, producing the same point.
So the choice is not about the payload. It is about what holding — or re-establishing — a connection costs the thing doing the sending.
The short answer
| Your device | Use |
|---|---|
| A laptop, a server, a cron job | HTTP |
| A mains-powered Pi or mini PC | HTTP |
| A phone or tablet | HTTP — or nothing at all |
| A battery microcontroller (ESP32, nRF) | MQTT |
| Anything on a metered cellular plan | MQTT |
| Anything sending more than once a second | MQTT |
| A thing you will build in the next 20 minutes | HTTP |
If you are undecided, that last row is the real answer. Start with curl, get a dot moving, and switch later — the field names are identical, so the migration is the transport and nothing else.
Why the handshake decides it
An HTTPS request that delivers one GPS point does this first:
- TCP three-way handshake — one round trip
- TLS handshake — one to two more round trips, and a few kilobytes of certificate
- Request headers — a few hundred bytes
Then it sends about forty bytes of actual position, and tears the whole thing down again. On a cellular link with 100 ms of latency, that is roughly a third of a second of radio-on time per point, most of it spent negotiating.
MQTT pays the same cost once. After the connection is up, a published point is a two-byte fixed header plus the topic plus the payload. No handshake, no headers, no teardown. A keepalive ping every minute or so, and that is all.
At one point every two seconds, HTTP re-negotiates 30 times a minute. MQTT negotiates once and then goes quiet between messages — which on a battery device is the difference between the radio idling and the radio working.
Where each one actually falls apart
HTTP breaks down when the radio never gets to sleep. On an ESP32 running from a lithium cell, per-point TLS handshakes are usually the largest line in the power budget — larger than the GPS module. You notice this as a tracker that lasts a day instead of a week.
MQTT breaks down when the connection is the fragile part. A vehicle drives through tunnels and dead zones; every reconnect is a fresh TLS handshake, and a client that reconnects badly can end up doing more handshakes than HTTP would have. Broker connections are also stateful in a way that shell scripts and serverless functions are genuinely bad at — there is nothing to hold the socket between invocations.
The honest summary: MQTT wins when the connection is long-lived and the device is power-constrained. HTTP wins when the sender is stateless, occasional, or being written this afternoon.
The same point, both ways
# HTTP
curl "https://gpstrack.dev/api/ingest?token=$TOKEN&lat=6.9271&lng=79.8612&speed=42"
# MQTT
mosquitto_pub -h gpstrack.dev -p 8883 --capath /etc/ssl/certs \
-u device -P "$TOKEN" \
-t "gpstrack/$SESSION_ID/gps" \
-m '{"lat":6.9271,"lng":79.8612,"speed":42}'
Identical field names, identical aliases, identical bounds on custom channels. That is enforced by a test asserting both transports produce the same point from equivalent payloads, which exists because they were once two copies of the same logic and drifted.
Four differences worth knowing before you commit
Errors. HTTP tells you what went wrong — 400 with a reason, 401 for a bad
token, 429 when you are over the rate limit. MQTT publish has no response
channel at all, so a malformed payload, an unauthorised topic or an over-rate
message is simply dropped. Debugging is meaningfully harder. Get the payload
right over HTTP first, then port it.
The rate limit is the same either way: one point per second per session,
burst of five. Over HTTP that is a 429 with Retry-After. Over MQTT it is
silence. If MQTT points are going missing and everything looks correct, this is
the first thing to check.
Topics are namespaced. A client may only publish under
gpstrack/<sessionId>/. Anything else is refused.
Subscriptions are refused entirely. This is an ingest-only broker — the dashboard reads over HTTP. Denying all subscriptions is what stops one client reading another's stream, and it means MQTT here is a one-way pipe, not a bus.
Ports and TLS
| Host | Port | Transport | |
|---|---|---|---|
| HTTP | gpstrack.dev |
443 | HTTPS |
| MQTT | gpstrack.dev |
8883 | MQTT over TLS |
Port 8883 is the conventional port for MQTT over TLS, and it is bound directly rather than sitting behind the web server — MQTT is raw TCP and cannot pass through an HTTP proxy's request handling.
There is no plaintext 1883 listener in production. The session token travels as the MQTT password, so an unencrypted broker would be publishing your write credential to every hop on the path.
What neither of them is for
If the device that has the GPS is also the device showing the map — a phone recording a run, a ride or a taxi journey — both protocols are the wrong answer. There is no round trip worth making. Track Me records to the phone's own storage and never contacts the server at all, which removes the queue, the token, the rate limit and the outage handling in one move.
The transport question only exists when the tracker and the display are two different machines.
Where to go next
- Track anything with one HTTP request
- Stream GPS from an ESP32 over MQTT
- Live-track a Raspberry Pi with a USB GPS dongle