Engineering

Designing a Real-Time Floor Graph Engine for Sub-300ms Dispatch Decisions

11 min read
Abstract network graph visualization representing a warehouse floor topology

The floor graph is the piece of Botpylon's architecture that everything else depends on. The congestion prediction model is only as accurate as the floor graph it reads. The task optimizer can only be as good as the path costs the floor graph provides. The dispatch API is only as fast as the floor graph query that precedes each assignment decision. If the floor graph is stale or slow, the dispatch system is stale and slow, regardless of how sophisticated the optimization logic is on top of it.

We've spent considerable time on this component. This post is a description of the design decisions we made, the problems we ran into building the first version, and what the current architecture looks like. It's written for engineers who are thinking about similar problems, not as a sales pitch for Botpylon specifically.

What the Floor Graph Represents

The floor graph is a weighted directed graph where nodes represent physical locations on the warehouse floor and edges represent traversable paths between those locations. Each edge has a base weight (derived from physical distance) and a dynamic weight adjustment (derived from current and predicted robot density on that path segment).

Node types in our graph: pick locations (individual bin positions), transit nodes (aisle intersections and corridor waypoints), deposit stations, charging stations, and staging areas. The static graph, representing the physical floor topology, is initialized from the floor plan at setup time and rarely changes unless the physical layout changes. The dynamic state, robot positions, edge weight adjustments, blocked path segments, is updated continuously from the adapter layer.

The floor graph needs to answer two query types efficiently. Path cost queries: what is the current weighted cost of the path from node A to node B? These are used during task assignment to compare candidate assignment options. Occupancy queries: what is the current robot density on a given path segment or within a given zone? These feed the congestion prediction model and zone balancing logic.

Both query types need to execute in under 20ms in our system, since multiple queries are batched per dispatch decision and the full decision pipeline has a 300ms budget. A slow graph query is a hard blocker for the dispatch latency target.

Version 1 and Why It Didn't Work

Our first floor graph implementation used a straightforward adjacency list representation with a full Dijkstra path search per query. This worked correctly at low fleet sizes and low query rates. At 80+ robots with a telemetry update rate of twice per second per robot, it didn't hold up.

The problem was write contention. With 80 robots each emitting position updates twice per second, the graph was receiving 160 writes per second. Each write updated the robot's node position, which required recalculating the edge weights for all edges connected to the robot's previous node and new node. In the worst case, when many robots were near a high-connectivity transit node, a single position update triggered recalculation of a dozen edge weights. Simultaneously, path cost queries were running against the graph. The read/write contention at high robot density caused query latency to spike from 8ms to 80-120ms under load. That was incompatible with our dispatch target.

We considered a few approaches: optimistic locking, read-through caching, and a copy-on-write structure. We ended up with a different design that separated the write path from the read path more fundamentally.

The Current Design: Separated Write and Read Paths

The current floor graph uses a split architecture. There is a write-side graph that receives all telemetry updates and maintains current robot state. There is a read-side snapshot graph that is used for all path cost and occupancy queries. The snapshot is published from the write-side graph on a fixed cadence (every 200ms) as an atomic operation, producing a consistent graph state at a known timestamp. Queries read from the snapshot exclusively; they never touch the write-side graph.

The snapshot publication is the critical operation. It has to be fast enough to complete within the 200ms cadence without blocking either the write-side updates or the read-side queries. We implement it as a copy of the dynamic state only: robot positions, edge weight adjustments, and blocked segment flags. The static topology (node positions, base edge weights, connectivity) is read-only and shared between the write-side and all snapshots. The dynamic state copy is small enough (at 150 robots, roughly 40KB) to copy in under 2ms.

This design gives queries a consistent snapshot that is at most 200ms old at the time of query. The maximum staleness is predictable and bounded, which matters for the dispatch latency guarantee. The write path is never contended by readers. The snapshot publication is a lightweight operation that doesn't interrupt either side.

The tradeoff: query results may reflect a state up to 200ms old. For path cost queries used in task assignment, 200ms staleness is acceptable because robot positions change predictably within that window (at 1.5 m/s, a robot moves at most 30cm in 200ms, which is unlikely to meaningfully change a path cost calculation). For occupancy queries used in real-time congestion alerting, 200ms staleness is also acceptable because congestion events develop over seconds, not milliseconds.

Edge Weight Calculation

The base edge weight is Euclidean distance between the nodes, adjusted for path type. Aisle paths are weighted at 1.0x distance. Main transit corridors at 0.9x distance (faster travel, worth taking slightly longer routes to reach them). Tight aisle sections where robots must travel single-file at 1.4x distance (higher effective cost even if physically short). These base weights are set at floor initialization and validated against actual robot travel time measurements.

The dynamic weight adjustment is applied on top of the base weight and is the piece that makes the graph congestion-aware. We compute a density score for each path segment: the number of robots whose current trajectory intersects that segment within the next 30 seconds, weighted by how close they are to the segment. A density score above the comfortable threshold for that segment type triggers a weight multiplier. The multiplier scales from 1.0 (no adjustment) to 2.5 (severe congestion, route strongly discouraged but not blocked) continuously as density increases.

The "robots whose trajectory intersects this segment within 30 seconds" calculation is the expensive part. We can't run full path predictions for 150 robots every 200ms. Our approximation: each robot's current velocity vector is extrapolated forward for 30 seconds using simple linear projection (ignoring turns). Robots within 15 meters of a path segment in their projected trajectory are counted toward that segment's density score. This is a rough approximation, but it's fast (sub-millisecond per robot) and directionally correct. It tends to slightly overestimate density in curved-path situations and underestimate it at intersection points. The overestimation is the safer error, since it biases the graph toward routing robots away from occupied areas.

Path Blocked Events

When a robot enters the "blocked" state (stopped due to obstacle or safety event), we mark the path segment the robot is occupying as temporarily blocked. All subsequent path cost queries treat blocked segments as impassable. The blocked mark includes a timeout: if the robot doesn't clear within 90 seconds, the blocked mark is downgraded to a high-cost (not impassable) segment and an alert is generated for the operations team. This prevents a single stalled robot from permanently isolating a floor zone.

The 90-second timeout was set after observing that genuine blocked-but-clearing events (robot stopped for another robot, brief safety stop) resolve within 20-30 seconds in almost all cases, while robot faults that require human intervention typically remain unresolved for longer. The 90-second threshold gives a comfortable margin for normal clearance events while escalating genuine stalls before they significantly impact throughput.

Floor Graph Initialization and Maintenance

The static topology is initialized from a floor plan file in a standard CAD exchange format, processed by a topology extraction tool that identifies aisle paths, intersection nodes, pick location positions, and deposit station locations. This initialization step takes 10-30 minutes depending on floor complexity and is done once at setup, with updates required only when the physical layout changes.

One complexity we underestimated: floor plans are often outdated relative to the actual floor. Racking gets moved. Aisles get narrowed by added storage. New pick locations get added. During our first few deployments we used the floor plan as ground truth and ended up with graph topology that included paths robots couldn't actually traverse. We now run a 20-minute validation step before go-live: we send each robot type on a validation route that traverses all major path segments and log any positions where the robot's actual path deviates significantly from the graph's predicted path. Deviations above 40cm trigger a topology review for that segment. It's unglamorous but necessary.

Query Performance at Scale

Under production load on a 150-robot floor with a task assignment rate of 12-15 assignments per second, our floor graph query latency profiles as follows: p50 at 4ms, p95 at 11ms, p99 at 18ms. We've never seen a query exceed 30ms in production operation. That leaves ample headroom within the 300ms dispatch budget for the optimization and routing layers above the graph.

The snapshot approach is central to this performance. By guaranteeing that all reads come from a consistent, non-mutating snapshot, we eliminate the lock contention that made our first version slow under load. The 200ms snapshot cadence adds bounded staleness but removes the variable latency penalty that contended graph structures impose.

We're not claiming this design is optimal or that other approaches couldn't achieve similar results. What we are confident of is that separating the write path from the read path, with a fast atomic snapshot operation bridging them, is a design that holds up under real warehouse operating conditions. The floor graph has been the most performance-sensitive component of the system to build correctly, and it's the piece that most directly determines whether the dispatch latency guarantee is achievable in practice.