Build a fleet tracking dashboard: troubleshooting guide
A practical troubleshooting guide for a fleet tracking dashboard built from GPS position events, PostgreSQL with PostGIS, a WebSocket or similar live-update channel, GeoJSON, and MapLibre GL JS. It focuses on isolating failures across ingestion, storage, spatial computation, transport, and browser rendering rather than treating the map as the entire system.
Fleet tracking dashboards fail at boundaries: a device timestamp is mistaken for server time, latitude and longitude are reversed, a spatial query uses the wrong units, a WebSocket silently stops delivering updates, or a browser map receives valid GeoJSON but cannot render it because the container has no height. Troubleshoot the system in layers. First prove that a valid position event exists, then verify the stored geometry, then verify the current-state API, then verify live delivery, and only afterward debug map styling and interaction.
How EDK fits
For: Developers and GIS engineers building or operating a browser-based fleet tracking dashboard with GPS or telematics events, a spatial database, a live update channel, and a web map.
Fleet tracking is a concrete example of computing about the physical world. A vehicle position is not useful only as a dot on a map; it becomes operationally meaningful when it can be related to roads, depots, service areas, facilities, incidents, restricted zones, and other geographic features. The troubleshooting workflow in this guide keeps those relationships explicit: coordinates need a known reference system, timestamps need clear semantics, and proximity or containment results need documented units and predicates. That is the same general problem addressed by a world-model perspective: finding physical entities, understanding their context, and computing relationships across them. The guide does not assume that E2 supplies telematics, transport, routing, or dashboard components. Instead, it shows where a fleet application may eventually need a broader geographic representation than vehicle points alone.
- •Use a world-model interface to place live vehicle observations in the context of physical features such as roads, depots, charging stations, hydrants, parks, waterways, or construction areas, provided the relevant data is available and properly attributed.
- •Use geographic computation to rank or filter vehicles by proximity to facilities, service areas, incidents, or infrastructure, while preserving the distinction between straight-line distance, road-network distance, and operational availability.
- •Use a shared geographic representation to compare vehicle movement with changing physical conditions, such as road closures, flooding, construction activity, or service-request clusters, without implying that the dashboard itself automatically supplies those external datasets.
The EDK outcome
Before you start
A defined position-event schema with vehicle identifiers and explicit observed_at and received_at timestamps.
A test vehicle or fixture event that can be replayed safely.
Access to application logs, database inspection, API responses, and browser developer tools.
PostgreSQL with PostGIS if the dashboard performs spatial queries in the database.
A map renderer and style configuration, such as MapLibre GL JS, if the problem reaches the browser layer.
A written policy for stale positions, out-of-order events, invalid coordinates, duplicate events, and low-accuracy fixes.
Learning objectives
Trace a fleet position from raw event through database, API, live transport, and browser rendering.
Identify longitude-latitude order, SRID, projection, and distance-unit errors.
Separate current vehicle state from historical observations and display updates.
Diagnose WebSocket freshness, reconnect, duplicate, and overload problems.
Choose when GeoJSON is sufficient and when a tiled or more specialized delivery strategy is warranted.
Verification
The same fixture event has the same longitude, latitude, and timestamps in raw logs, database output, API response, live message, and rendered feature.
The current-state table does not regress when an older event arrives unless a documented correction workflow allows it.
At least one spatial query has an explicit unit test for coordinate order, SRID, and distance units.
A disconnected browser can recover current positions from the snapshot endpoint before resuming live updates.
The browser remains responsive under a representative fleet size and update rate; message volume, rendering time, and memory use are measured rather than assumed.
Stale, invalid, low-accuracy, and unavailable positions are visibly distinguishable from healthy live positions.
Troubleshooting
All vehicles disappear after an update: inspect whether the client replaces the entire FeatureCollection with an empty or partially parsed message. Log feature count before and after each setData call.
One vehicle jumps between distant locations: check longitude-latitude order, device coordinate encoding, duplicate vehicle identifiers, and whether an old event overwrites a newer one.
The database query is slow: verify a spatial index, use ST_DWithin for filtering, bound the time or vehicle set, and inspect whether an implicit cast prevents index use. Do not optimize by changing units without validating results.
The API is current but the browser is stale: inspect WebSocket readyState, close events, reconnect behavior, proxy idle timeouts, and whether the server publishes only on new events while the client needs periodic freshness checks.
The browser becomes unresponsive: measure message rate and rendering time, coalesce updates, avoid redrawing historical tracks with every point, and consider clustering or vector-tile delivery for large point sets.
The map initializes but has no visible content: check the container's computed dimensions, map center and zoom, style load errors, source errors, layer visibility, and whether the coordinates are within the current viewport.
Nearby results are wrong around boundaries: distinguish ST_DWithin from ST_Within. Proximity and containment answer different questions, and a vehicle may be near a zone without being inside it. (postgis.net)
Updates are duplicated after reconnect: assign sequence numbers or event IDs, make client application idempotent, and decide whether the server sends a snapshot followed by events or attempts to replay a precise range.
The dashboard shows a vehicle as moving when the device is offline: separate last-known position from current freshness. A historical point is not evidence of a current observation.
Next steps
Add automated contract tests for coordinate order, timestamp parsing, duplicate handling, stale thresholds, and GeoJSON serialization.
Add a replay tool that can feed recorded events through ingestion without contacting production devices.
Instrument end-to-end latency from observed_at to database commit, API availability, live publication, browser receipt, and rendered update.
Introduce a geographic context layer only after the vehicle stream is trustworthy: roads, facilities, service areas, incidents, infrastructure, or environmental features should each have documented provenance, update cadence, and spatial semantics.
When live points are operating reliably, evaluate whether a broader physical-world model would simplify cross-dataset questions such as finding nearby assets, comparing routes with infrastructure, or analyzing movement against changing geographic conditions.
What you need before debugging
Choose one known-good vehicle and one short time window before investigating the whole fleet. You need access to the raw inbound payload, the process that validates and persists it, the database row representing the latest position, the current-state HTTP response, the live-update channel, and the browser developer console. Without those checkpoints, a blank or stale map cannot be localized to a single layer.
Define the minimum event contract. A practical example is: vehicle_id, event_id, observed_at, received_at, longitude, latitude, speed, heading, accuracy, and source_status. observed_at is when the device says the position was measured; received_at is when your service accepted it. Do not silently replace one with the other. The difference is essential for detecting delayed cellular delivery and for deciding whether a vehicle is stale.
- •A reproducible test payload with a fixed vehicle_id and event_id.
- •A database with PostGIS enabled if you need spatial queries.
- •A browser map container with explicit width and height.
- •A health endpoint or structured logs for ingestion, persistence, and live publication.
- •A documented stale threshold, such as five minutes, chosen for the device reporting interval rather than copied from an example.
Step 1: validate the position-event contract
Start by logging the event before any transformation. Check whether longitude is between -180 and 180, latitude is between -90 and 90, the timestamp parses as an explicit time zone, and the vehicle identifier is not empty. Reject impossible coordinates instead of storing them as if they were valid. Also check whether the device sends a GPS fix, a network-derived location, or an old cached fix; the dashboard should expose or at least preserve that distinction.
A common failure is accepting valid-looking but semantically wrong data. Coordinates such as [40, -74] are legal GeoJSON, but they represent a different place from [-74, 40]. If the map is centered on the fleet's expected region and one vehicle appears far away, inspect the raw array before changing the map projection or basemap.
- •Log event_id, vehicle_id, observed_at, received_at, coordinate pair, validation result, and rejection reason.
- •Deduplicate by a stable event identifier when the provider supplies one; otherwise define a documented device-time and sequence strategy.
- •Record a data-quality status such as valid, stale, low_accuracy, rejected, or corrected.
- •Never use a browser display timestamp as the authoritative observation time.
Step 2: verify PostGIS storage and spatial units
Construct the point with longitude as X and latitude as Y. PostGIS documents ST_MakePoint as accepting X and Y coordinates, with longitude as X and latitude as Y for geodetic coordinates; ST_SetSRID labels the coordinate reference system but does not transform the coordinates. (postgis.net)
A simple schema can keep the raw numeric values for inspection and a geography column for meter-based proximity checks. The exact schema is an implementation choice, but the important troubleshooting property is that the geometry or geography type, SRID, and distance units are unambiguous.
- •Check that every stored point has the expected SRID.
- •Check whether a query uses geometry units or geography meters.
- •Run EXPLAIN on a slow proximity query and confirm that the intended spatial index is eligible.
- •Use ST_Within only when containment is the actual question; a vehicle near a service-area boundary may not be within it. PostGIS requires compatible coordinate systems for this relationship. (postgis.net)
Step 3: isolate the current-state API from live updates
Before testing WebSocket delivery, make the dashboard work from a normal HTTP snapshot. The endpoint should return a bounded, validated FeatureCollection or another clearly documented representation. A snapshot lets a reconnecting browser recover without replaying every historical event and gives you a stable test target for curl, browser tools, and automated checks.
Example response shape:
- •Test the snapshot with curl or an API client before opening the browser.
- •Return server-generated freshness metadata so the client can distinguish no update from stale data.
- •Use stable feature IDs so a vehicle can be updated rather than duplicated.
- •Keep historical tracks separate from current markers to avoid redrawing entire paths for every position event.
Step 4: verify live delivery and browser rendering
Only after the snapshot works should you test live updates. A browser WebSocket client can open a connection, receive messages, and close it, but the standard WebSocket interface does not provide automatic backpressure. If updates arrive faster than the page can process them, queued messages can consume memory or make the page unresponsive. (developer.mozilla.org)
Use a server-side publication policy that matches the display need. For example, the backend may retain every event but publish only the newest position per vehicle within a short display interval. The exact interval depends on fleet operations, device frequency, network cost, and user expectations; there is no universal correct value.
- •Confirm the WebSocket reaches OPEN and log close codes and reconnect attempts.
- •Use wss:// when the page is served over HTTPS; avoid mixed-content connections. (developer.mozilla.org)
- •Coalesce frequent updates on the server or render at a controlled cadence on the client.
- •Call setData with a valid FeatureCollection and preserve stable vehicle IDs.
- •Use clustering or vector tiles when point counts exceed what the browser can comfortably redraw.
Workflow
Create a known-good fixture
Create one valid event for a test vehicle and preserve the exact raw payload. Record the expected longitude, latitude, observed_at, and received_at. Replay this fixture through the normal ingestion path so each layer can be compared against a known answer.
{
"event_id": "fixture-001",
"vehicle_id": "truck-17",
"observed_at": "2026-09-09T12:00:00Z",
"received_at": "2026-09-09T12:00:02Z",
"longitude": -73.9857,
"latitude": 40.7484,
"speed_mps": 8.2,
"heading_deg": 90,
"source_status": "gps_fix"
}Validate and construct the spatial point
Reject impossible coordinate ranges, preserve both timestamps, and construct the point with longitude as X and latitude as Y. Keep raw fields while debugging so a malformed geometry can be traced back to the input.
SELECT ST_SetSRID(
ST_MakePoint(-73.9857, 40.7484),
4326
)::geography AS position;Check the latest stored position
Confirm that the current-state row contains the expected vehicle, timestamp, coordinate, freshness status, and geometry type. If the event is older than the stored observation, verify that the update policy intentionally rejects it.
SELECT
vehicle_id,
observed_at,
received_at,
ST_AsText(position::geometry) AS position_wkt,
ST_SRID(position::geometry) AS srid
FROM vehicle_current_position
WHERE vehicle_id = 'truck-17';Test proximity independently of the map
Run a small, explicit proximity query against a known facility or service area. Confirm that the distance unit is meters when using geography and that the query operands use compatible spatial representations.
SELECT v.vehicle_id, f.facility_id
FROM vehicle_current_position AS v
JOIN facilities AS f
ON ST_DWithin(v.position, f.position, 500)
WHERE v.vehicle_id = 'truck-17';Test the snapshot endpoint
Request the current-state API without loading the web application. Verify the content type, FeatureCollection structure, coordinate order, stable feature IDs, timestamps, and stale-status fields. A correct snapshot proves that the browser does not need to be involved in the first part of the investigation.
curl -i https://example.invalid/api/fleet/currentTest live delivery with a slow and a fast consumer
Connect one client that logs every message and another that intentionally renders less often. Compare sequence numbers, timestamps, duplicates, and gaps. If the server sends faster than clients can process, add coalescing, batching, or a controlled display cadence rather than allowing an unbounded queue.
const socket = new WebSocket("wss://example.invalid/stream/fleet");
socket.addEventListener("open", () => console.log("fleet stream open"));
socket.addEventListener("message", event => {
const message = JSON.parse(event.data);
console.log(message.vehicle_id, message.observed_at, message.sequence);
});
socket.addEventListener("close", event => console.warn("fleet stream closed", event.code));
socket.addEventListener("error", error => console.error("fleet stream error", error));Compare the rendered source with the API payload
If the API and WebSocket are correct, inspect the map source after each update. Confirm that the source ID exists, setData receives valid GeoJSON, the layer references the same source, and the map container has nonzero dimensions.
const source = map.getSource("vehicles");
if (!source) throw new Error("vehicles source is missing");
source.setData({
type: "FeatureCollection",
features: latestVehicles.map(v => ({
type: "Feature",
id: v.vehicle_id,
properties: {
vehicle_id: v.vehicle_id,
stale: v.stale,
observed_at: v.observed_at
},
geometry: {
type: "Point",
coordinates: [v.longitude, v.latitude]
}
}))
});Questions teams ask
Why does the vehicle appear in the wrong country or ocean?
Inspect the raw coordinate array and confirm that it is [longitude, latitude]. Then verify that the backend did not swap fields while constructing the PostGIS point. Check the SRID as well, but do not assume changing the SRID fixes swapped or transformed coordinates: ST_SetSRID labels coordinates; it does not transform them. (postgis.net)
Why is the dashboard stale even though the device is transmitting?
Compare observed_at with received_at and with the latest database row. The device may be sending delayed or cached fixes, the current-state updater may reject out-of-order events, or the live publisher may be disconnected while the snapshot endpoint remains current. Display freshness and source status separately so users can distinguish a stationary vehicle from a missing update.
Why does a PostGIS radius query return unexpected results?
Check whether the columns are geometry or geography, whether both operands use compatible coordinate systems, and whether the distance is expressed in the units you intended. With geography, ST_DWithin uses meters; with geometry, the distance uses the spatial reference system's units. (postgis.net)
Why is the map blank while the API returns valid GeoJSON?
Check the map container's computed width and height, style-loading errors, source and layer names, browser network failures, and the coordinate bounds. If the source is updated through setData, confirm that the object is a valid GeoJSON source value and that the layer references the same source ID. MapLibre's documentation provides GeoJSON source and map initialization examples for comparison. (maplibre.org)
Should every GPS event be sent directly to every browser?
Usually not. Retain the full event stream where operationally necessary, but publish a display-oriented stream that coalesces updates, limits history, and handles reconnects. The classic WebSocket API has no automatic backpressure, so an unbounded high-rate feed can overwhelm the browser. (developer.mozilla.org)
Keep exploring
build a fleet tracking dashboard
Start with the provider-neutral architecture, event model, storage approach, and current-state API before applying this troubleshooting workflow.
add vector tiles to a react app troubleshooting guide
Use this related guide when the fleet data is correct but the React and MapLibre rendering path has blank maps, source-layer, CSS, or lifecycle failures.
analyze spatial data with geopandas
Use this guide when troubleshooting expands from live display into offline track analysis, spatial joins, measurement, or validation.
Sources
RFC 7946: The GeoJSON Format
Authoritative IETF standard for GeoJSON objects, geographic features, coordinate order, and WGS 84 decimal-degree coordinates.
Open sourcePostGIS ST_DWithin documentation
Defines proximity semantics, geometry versus geography distance units, compatible coordinate systems, and index-aware bounding-box behavior.
Open sourcePostGIS guidance: Use ST_DWithin for radius queries
Provides the official rationale and examples for using ST_DWithin rather than ST_Distance or buffered intersection as the primary radius filter.
Open sourceMapLibre GL JS GeoJSONSource API
Documents GeoJSON sources, source updates through setData, clustering-related methods, and source behavior in the browser.
Open sourceMDN WebSocket API
Documents browser WebSocket behavior, connection methods, events, and the absence of automatic backpressure in the classic interface.
Open source