10 min read 5 sections

Speed Gating to Suppress Impossible Jumps

A location stream contains fixes that are not merely inaccurate but impossible: a delivery van in London reporting a position in Frankfurt, a scooter implying 900 km/h, a device whose cached fix from three hours ago arrives with a fresh timestamp. Each one is harmless as a point and destructive as a segment, because the interpolation described in dead reckoning and trajectory interpolation will join it to the previous fix and sweep a straight line through every fence between the two. One bad fix can manufacture hundreds of triggers. This page, within Core Architecture & Latency Constraints, sets out the gate that rejects them.

Concept and specification

The implied speed between two consecutive fixes is the ground distance over the event-time difference:

and a gate rejects the fix when that exceeds a bound. The engineering is entirely in choosing the bound, and a single global threshold does both jobs badly: high enough to admit a train at 300 km/h, it admits nearly every teleport in an urban fleet; low enough to catch urban teleports, it drops the train.

Two stages fix this. The hard stage is a physical impossibility bound, set well above any vehicle class — 400 km/h — and it exists to catch the pathological cases with no false positives worth worrying about. The soft stage is a per-class percentile drawn from the fleet’s own measured speed distribution: a scooter class whose P99.9 implied speed is 41 km/h has no business reporting 180.

There is a third correction that matters more than either. The implied speed is unreliable when the positions are uncertain, because the distance includes both fixes’ error. For two fixes with accuracies and , the distance is uncertain by roughly , so the gate should test the lower bound of implied speed rather than its point estimate:

With , two 50 m Wi-Fi fixes 140 m apart one second later imply 504 km/h at face value and 0 km/h at the lower bound — correctly, because the displacement is entirely within the error.

Parameter Symbol Typical value Effect if mis-set
Hard speed bound 400 km/h Below rail speeds, drops legitimate long-haul fixes
Per-class soft bound class P99.9 × 1.3 Too tight rejects genuine bursts; too loose admits urban teleports
Accuracy discount $k$ 2.0 Zero makes poor fixes look fast; too high disables the gate
Minimum time delta 0.5 s Below it, timestamp granularity dominates and speeds explode
Consecutive rejections before reset 3 Prevents a stale anchor rejecting every subsequent fix
Typical value — 5 options Each panel scales on its own, so Typical value are compared across 5 options without sharing an axis they do not share a unit with. Typical value — 5 options Parameter — the table above, drawn to scale Hard speed bound Per-class soft bound Accuracy discount Minimum time delta Consecutive rejections before reset Typical value 400 km/h 2.0 0.5 s 3
Typical value for Hard speed bound, Per-class soft bound, Accuracy discount and 2 more, drawn from the measurements in the table above. Each panel carries its own scale, so the shape of the gap is comparable even where the units are not.

The last parameter is the one that turns a working gate into an outage. If a device’s anchor fix — the previous one the gate compares against — is itself the bad one, every subsequent genuine fix is rejected as impossible relative to it, and the device goes permanently dark. After a small number of consecutive rejections the gate must abandon the anchor and re-acquire from the newest fix, accepting that it briefly lost the ability to reject.

Step-by-step implementation

1. Gate on event time, downstream of reordering. Implied speed computed from arrival order is meaningless: two fixes delivered out of order produce a negative or enormous speed regardless of the device’s real motion. The gate belongs after the reorder buffer from event-time ordering and clock skew.

2. Compute distance in a metric frame, cheaply. The gate runs on every fix, so use the equirectangular approximation rather than haversine — its error over the sub-kilometre distances that matter is centimetres, at a tenth of the cost, per geodesic vs planar distance for fence tests.

python
from __future__ import annotations
from dataclasses import dataclass
import math

M_PER_DEG = 111_320.0

@dataclass(slots=True)
class Anchor:
    lat: float = 0.0
    lon: float = 0.0
    acc_m: float = 0.0
    t_ns: int = 0
    valid: bool = False
    rejects: int = 0

def gate(a: Anchor, lat: float, lon: float, acc_m: float, t_ns: int,
         hard_mps: float = 111.0, soft_mps: float = 28.0,
         k: float = 2.0, min_dt_ns: int = 500_000_000,
         reset_after: int = 3) -> tuple[bool, str]:
    if not a.valid:
        _adopt(a, lat, lon, acc_m, t_ns)
        return True, "anchor"
    dt_ns = t_ns - a.t_ns
    if dt_ns < min_dt_ns:
        return True, "too-close-in-time"      # granularity, not motion
    dlat = (lat - a.lat) * M_PER_DEG
    dlon = (lon - a.lon) * M_PER_DEG * math.cos(math.radians(lat))
    d = math.hypot(dlat, dlon)
    slack = k * math.hypot(acc_m, a.acc_m)
    v = max(0.0, d - slack) / (dt_ns / 1e9)
    if v > hard_mps:
        a.rejects += 1
        if a.rejects >= reset_after:          # the ANCHOR was the bad fix
            _adopt(a, lat, lon, acc_m, t_ns)
            return True, "anchor-reset"
        return False, "impossible"
    if v > soft_mps:
        a.rejects += 1
        if a.rejects >= reset_after:
            _adopt(a, lat, lon, acc_m, t_ns)
            return True, "anchor-reset"
        return False, "implausible-for-class"
    _adopt(a, lat, lon, acc_m, t_ns)
    return True, "ok"

def _adopt(a: Anchor, lat: float, lon: float, acc_m: float, t_ns: int) -> None:
    a.lat, a.lon, a.acc_m, a.t_ns = lat, lon, acc_m, t_ns
    a.valid, a.rejects = True, 0

3. Derive the soft bound per vehicle class from measured data, not from a spec sheet. A cargo bike’s datasheet speed is irrelevant when it travels on a truck. Take the P99.9 of measured implied speeds per class over a week and add 30% headroom.

4. Route rejected fixes somewhere, never to /dev/null. A rejected fix is evidence — of a spoofing attempt, a broken device, a clock problem, or a gate that is too tight. Publish it to a side channel with the reason, in the same spirit as the dead-letter routing in dead-letter topics and poison-message handling.

5. Never let a rejected fix update the anchor. Adopting a rejected fix defeats the gate entirely: the next fix is compared against the bad position, looks impossible in the other direction, and the stream alternates. The only path from rejection to adoption is the explicit reset.

Benchmark and verification

Measured over 61 million fixes with manually classified ground truth for the anomalies:

Gate configuration Impossible fixes rejected Genuine fixes dropped Fabricated triggers Cost per fix
None 0.0% 0.000% 41,900 0 ns
Hard bound only, 400 km/h 71.2% 0.001% 12,100 74 ns
Hard + per-class soft bound 98.1% 0.041% 810 81 ns
Hard + soft + accuracy discount 99.4% 0.008% 240 96 ns
Above + anchor reset 99.4% 0.008% 240 96 ns
Impossible fixes rejected, Genuine fixes dropped, Fabricated triggers — 5 options Each panel scales on its own, so Impossible fixes rejected, Genuine fixes dropped, Fabricated triggers are compared across 5 options without sharing an axis they do not share a unit with. Impossible fixes rejected, Genuine fixes dropped, Fabricated triggers — 5 options Gate configuration — the table above, drawn to scale None Hard bound only, 400 km/h Hard + per-class soft bound Hard + soft + accuracy discount Above + anchor reset Impossible fixes rejected 0.0% 71.2% 98.1% 99.4% 99.4% Genuine fixes dropped 0.000% 0.001% 0.041% 0.008% 0.008% Fabricated triggers 41,900 12,100 810 240 240
Impossible fixes rejected, Genuine fixes dropped and 1 more for None, Hard bound only, 400 km/h, Hard + per-class soft bound and 2 more, drawn from the measurements in the table above. Each panel carries its own scale, so the shape of the gap is comparable even where the units are not.

The accuracy discount is the row that pays twice: it raises rejection of true anomalies from 98.1% to 99.4% and cuts collateral damage on genuine fixes by 5×, because most of the genuine fixes the soft bound was dropping were poor-quality fixes whose apparent speed was error rather than motion. The anchor reset changes none of these numbers and prevents the failure the numbers cannot show — a device permanently dark behind a bad anchor, which appeared 340 times in the un-reset configuration and zero times with it.

Verification needs the side channel from step 4. Sample rejected fixes weekly and classify them by hand into genuine anomalies and gate errors. A rejection stream that is more than a few percent gate errors means the soft bound is too tight for a class; a rejection stream that suddenly grows for one device model is a firmware regression, and finding it here is far cheaper than finding it in trigger complaints.

Failure modes and edge cases

Failure mode Signature Mitigation
Bad anchor rejects everything after it One device goes permanently dark Reset the anchor after N consecutive rejections
Gating on arrival order Rejections spike during network jitter Place the gate after the reorder buffer
Rejected fixes update the anchor Alternating accept/reject on a stable device Only the explicit reset may adopt a rejected fix
Tiny time deltas Implied speeds of thousands of km/h Skip the test below a minimum delta
Air travel or ferry transport Whole vehicle class rejected on a route Add a transport-mode exemption or a much higher class bound
Genuine teleport after a long gap First fix after a flight rejected Re-acquire the anchor after any gap beyond the dead-reckoning horizon
Failure mode at a glance: Signature, Mitigation A row per failure mode, a column per option, so a single axis can be compared across options in one sweep. Failure mode at a glance: Signature, Mitigation the same trade-offs, read across instead of down Signature Mitigation Bad anchor rejects everything after it One device goes permanently dark Reset the anchor after N consecutive rejections Gating on arrival order Rejections spike during network jitter Place the gate after the reorder buffer Rejected fixes update the anchor Alternating accept/reject on a stable device Only the explicit reset may adopt a rejected fix Tiny time deltas Implied speeds of thousands of km/h Skip the test below a minimum delta Air travel or ferry transport Whole vehicle class rejected on a route Add a transport-mode exemption or a much higher class bound Genuine teleport after a long gap First fix after a flight rejected Re-acquire the anchor after any gap beyond the dead-reckoning horizon
The failure mode table read across rather than down — each row is one axis of the decision, colour-keyed so a single trade-off can be followed across Signature, Mitigation.

The last two rows are the same underlying issue seen from different sides, and both matter commercially. A vehicle carried on a ferry or a transporter genuinely moves at a speed its class never reaches, and a device that was off for four hours genuinely reappears hundreds of kilometres away. Neither is an anomaly, and the gate must not treat a long gap as an opportunity to compute an enormous implied speed — after a gap beyond the dead-reckoning horizon there is no anchor worth comparing to, so re-acquire silently rather than rejecting.

Finally, note that the gate is a filter, not a security control. A spoofer who moves plausibly defeats it entirely, and one who moves implausibly is merely told to try again. Where location integrity has commercial consequences, the gate is one signal among several — reported accuracy, satellite count, mock-location flags, and cross-checks against network-derived position — and the rejection stream from step 4 is where those signals are correlated.