Split//Time

How it works

The recorder, the rules and the known faults · as of 2026-10-13

SplitTime records the telemetry Forza Horizon 6 sends while you drive, works out where each lap or run starts and ends, and compares drivers' best times. The game sends physics and a few clocks. It does not send the track name, the event type, the official result or whether a lap was valid. Everything on this site beyond the raw numbers is inferred, and this page says how, and where the inference is currently wrong.

1. The pieces

Two processes share one SQLite file. The collector must stay up, because a packet that arrives while it is down is lost for good. The website can be rebuilt at any time.

The game, on each driver's PC"Data Out" sends one 324-byte UDP packet per rendered frame (60 to 140 a second), one way, unencrypted, no login
one UDP port per driver: the port is the driver's identity
Collectorparse packet → one SessionTracker per driver (app/recorder/laps.py) → buffered write every second
frames, laps, routes
SQLite file (WAL)tables: sessions, frames, laps, routes, session_groups, edits, attempts, drivers
read; small writes
Websitepages and /api/*. Asks the collector what is recording before any heavy write
every frame, plus live delta
Live socket/ws/live, served by the collector directly
HTTPS through the site's front door
BrowserViewing is public. Anything that changes data needs a signed-in, allowed user
Data path. The raw packets are stored unchanged, so any session can be replayed through newer detection code later ("Reprocess" on the Analysis page).

2. What the game sends, and what it leaves out

FieldWhat it isWhat the recorder uses it for
IsRaceOn1 while driving, including free roam. 0 in menus and pauseStarting and ending sessions
CurrentRaceTimeEvent clock. Starts at 0 when the event loadsSplitting sessions, timing runs that have no lap clock
LapNumber, CurrentLap, LastLap, BestLapThe game's own lap counter and lap clock. Dead (all zero) in some event typesLap boundaries and lap times on circuits
DistanceTraveledOdometer along the route. Normalised: it reads about 5950 at the end of every completed lap or run, whatever the course's real length. Negative on the run-up before a start lineAligning laps for comparison, detecting launch and finish, route length
RacePositionPlace in the field. 0 in free roam"Gridded": proof this is a race and not free roam
Position, yaw, speedWorld coordinates in metresMaps, route fingerprint, geometric lap detection
Acceleration, suspension, tyre slipPer frame, per wheelContact, jumps, surface guess

Not in the packet: track or event name, event type (race, Rivals, drift, drag), the result screen's time, a "lap invalid" or off-track flag, the driver's identity, other cars.

3. Sessions

A session is one continuous attempt at one event. Restarting an event makes a new session.

Packet with IsRaceOn = 1 and no open session
open a session, open lap 0
Recordingevery packet is buffered and stored; the lap logic runs on each one
race clock jumps back by more than 10 s to under 5 s
Splitclose this session, open a new one (event restart)
15 s of IsRaceOn = 0, or 15 s of silence
Endtry the end-of-stream finish rules (section 5)
at the end
At least one timed lap or runkeep; tag wet/surface; group into a sitting
No timed lapif it was a started event, note one attempt (no packets). Then delete the session and its packets and log one diagnostic line. With "keep discarded" on, keep it instead
Session life cycle. Free-roam driving also has IsRaceOn = 1, so it opens sessions too. They end with no timed lap and are deleted.

Attempts

Every started event is one attempt, however short. A session that ends with a timed lap or run is a finished attempt. One that ends without is given up: its packets go, but one small row stays with the driver, the car, where it started and how far it got. "Started" means a race from a grid, or a lap-timed event, whose odometer left the line. The route of a given-up attempt is worked out from its start position: the one recorded route that starts within 120 m, else the one catalogue course, else "Unknown route".

4. Laps on a circuit

Where the game runs its lap fields, the recorder trusts them:

While a lap is open the recorder keeps a trace of (distance into lap, elapsed time). The live delta is the current lap's time at this distance minus the session-best lap's time at the same distance. Comparing by distance, not by clock, is what makes two laps comparable corner by corner.

5. Finishing an event

This is the hardest part and the source of most faults. The game ends an event without incrementing LapNumber, and a sprint, drag or touge run never has a lap boundary at all. The recorder looks for a finish in this order. Rungs 1 to 3 are checked on every packet; rungs 4 to 6 only when the stream has stopped.

LastLap changed, LapNumber did notthe game posted a time at the line
Lap closed with the game's timefinish: confirmed
Odometer hard-reset to near zerowas above 500, now under 200, fell by more than 1000 in one frame, car did not teleport, LapNumber never rose, and the car was gridded or launched. A gridded run must also have covered the full route (5861 units)
Run closed at the previous packettime = that packet's race clock − clock at launch. finish: confirmed
Race clock frozen for 1.5 safter it has passed 5 s, while IsRaceOn is still 1
Marked finishedthe run is timed frozen clock − launch when the session ends. Undone if the clock resumes and the car drives 500 units further. A gridded run must have covered the full route when the clock froze
Stream ended inside a geometric crossingsee section 6
Lap closed at the closest approachflag: cutoff
Stream ended on a gridded runlaunched, LapNumber never rose, the full route covered (5861 units), last speed above 5 m/s, run longer than 10 s
Run timed to the last packetlast race clock − clock at launch. flag: cutoff
Stream ended on a circuit's open lapthe lap covered at least 97% of this session's typical lap
Final lap recoveredlap clock + remaining distance ÷ last speed (2 s at most)
Finish detection. Rungs 1 to 3 are separate signals and whichever arrives first closes the run; rungs 4 to 6 are tried in order at the end. If nothing matches, the open lap is stored without a time, and a session with no timed lap is deleted.

Where the clock starts for a run

"Launch" is the packet where the odometer first reaches 1.0. The code assumes that is the moment the lights go out on the grid. On the gridded sprints and drag runs recorded so far, the grid sits at a negative odometer reading (−70.8 on one sprint), so the odometer reaches 1.0 at the start line, not on the grid. Recorded run times are therefore line-to-line. See fault F5.

6. Events with no lap clock (World Time Attack)

Some events send no lap fields at all. Laps are then found from position alone.

Odometer leaves zerothe car's position is stored as the anchor; lap 1 starts here
speed passes 10 m/s
Launch heading stored
car gets more than 120 m from the anchor
Armed
car comes back within 60 m, heading within about 75° of launch, at least 500 units driven
Track the closest approachremember the packet nearest the anchor
car leaves the 60 m circle
Lap closed at the closest-approach packettime = race clock there − race clock at the lap's start. The trace is cut at that packet, and the next lap starts from it
Geometric lap detection. A single-frame jump of more than 250 m is a fast travel and switches this off. The odometer collapsing by more than 1000 ends the run.

7. Lap flags

FlagHow it is detectedEffect
rewindThe lap clock falls more than 0.5 s below its highest reading while the odometer is not increasingCannot be a best
contactGround-plane acceleration above 45 m/s² that is also an impulse: a rise of 30 m/s² within one frame, or a peak above 98 m/s². Ignored while airborne and for 0.35 s after landingCannot be a best
cutoffThe time was taken from the last packet, with no finish signal (rungs 4 and 5)Shown as "est."; counts only if the full route was covered

Flags can be corrected by hand on the Analysis page. Corrections are stored separately, keyed by time, and survive a reprocess.

8. Routes and their names

The first timed lap of a session produces a fingerprint: start position, length on the odometer, and the width and depth of the ground covered. It is matched against known routes.

An existing route matchesstart within 120 m, length within 5%, both spans within 15% (or 50 m)
Session joins that route
No route matches
A new route row is createdwith no name
The route has no name and a catalogue entry matchessame three tests against a bundled list of 77 official courses
Named from the cataloguea name typed by a person is never overwritten
Route matching and naming. A route that matches nothing in the catalogue is shown as "Unnamed route #n".

Because the odometer is normalised to about 5950 on every course, the length test separates nothing among completed runs. Start position and span do the work. The length test matters in one case only: a run that stopped short. Such a run is no longer timed (faults F3 and F4), so it no longer reaches this step.

9. Sittings

Restarting an event creates a new session, so an evening on one sprint arrives as many one-run sessions. When a session closes, finished sessions with the same driver, route and car, each starting within 2 hours of the one before, are grouped into a sitting. A sitting is an index entry: no lap or packet is moved. A session someone takes out of a sitting is never regrouped. Bests never depended on this; it only tidies the lists.

10. Which time counts as a best

No time
Not a bestincomplete
Excluded by hand
Not a bestexcluded
Flagged rewind or contactor any flag other than cutoff
Not a best
Flagged cutoff and not shown to have covered 5861 units98.5% of 5950, read from the lap's last packet. If that packet cannot be read, the distance is unproven and the time does not count
Not a bestroute not completed
Anything else
Candidatefastest candidate per driver, per route and car class, is the headline best. Per-car and route-overall bests are listed beneath
The best-lap rule (app/rivalry.py). The full-route test applies only to cutoff times; a time with a finish signal is assumed to be a finished run. Fault F4 was a case where that assumption was false.

This is the closest available match to the game's Rivals rule. The game's own verdict is not in the packet, and leaving the track is not detected at all.

11. What each page reads

PageSourceNotes
LiveThe collector's socketEvery packet as it arrives, plus the live delta
AnalysisSessions, laps and stored packetsLap tables, maps, traces; rename, flag, exclude, reprocess, delete
Rivalry/api/rivalryEvery lap with the verdict from section 10, which reads each lap's last packet once; the page only sorts and filters
Stats/api/statsThirteen sections computed from the session, lap, route and attempt tables. It reuses the Rivalry verdicts, so it reads the last packet of each lap once (then cached) for the full-route test. No statistic scans a whole trace yet
Replay/api/laps/{id}/data, /api/routes/{id}/corners, /api/corners/comparePosition and speed per packet for up to six laps, played against each lap's own clock. Corners are placed by route position; with two laps the comparison adds each corner's time difference and minimum speeds

12. Where it fails

For a worked example of a recording that looked broken but wasn’t, and of what truly can’t be recovered, see Three laps found.

Each of these is a property of the software, found in real recordings. The figures are from the captures that exposed them. "Fixed" means fixed in the code: sessions recorded before a fix keep their old result until they are reprocessed.

FaultSeen asCauseState
F1A complete run is deletedA 155 s gridded sprint, odometer +5952, discarded with no timed lapThe race clock paused at 8.6 s before the run. Rung 3 latched "finished", which made the run time under 10 s and switched rung 5 offfixed The latch is undone when the clock resumes. A run already deleted cannot be recovered
F2One circuit becomes two routesThe same circuit stored with length 5945 on one row and 6275 on anotherA geometric lap is closed only after the car leaves the 60 m circle, and the trace past the line was counted in the lap's length. 6275 is outside the 5% tolerancefixed The trace is cut at the crossing
F3An abandoned run got a time and its own routeA gridded sprint that stopped at odometer 4080 (69%) while rolling at 22 km/h, shown as an unnamed routeRung 5 treated "gridded, over 500 units, still moving faster than 5 m/s" as a finish at the line. A driver quitting while rolling met all of it. The short length then failed the 5% test, so a new route was createdfixed Rung 5 now needs the full route
F4An abandoned run got a confirmed timeThe stream paused 5.5 s at odometer 2483 (42%) with the car at 1.3 m/s, then the odometer read 0 with the car in the same place. Also shown as an unnamed routeRung 2 called any hard reset of the odometer a finish. It checked that the car did not teleport, not that it reached the end. Because the time was "confirmed", the full-route test in section 10 was skippedfixed For gridded runs, rung 2 now needs the full route. Runs with no lap clock (section 6) are unchanged
F5Sprint and drag times are probably shortA drag run recorded as 14.449 s while the game's own CurrentLap field reads 16.7 s in the last packet. A sprint recorded as 2:52.7 against about 2:55.1 on the game clockThe clock is started when the odometer reaches 1.0, which on these events is the start line, not the grid (section 5). The gap is the time from the grid to the lineopen Not yet checked against a result screen
F6A real course has no nameComplete runs on a dead-straight 1 km strip, about 14.4 s each, shown as an unnamed routeThe catalogue lists 77 courses and that strip is not among themopen Needs the in-game name
F7Sessions are deleted for reasons nobody can reconstructSessions of 125 s and 184 s, not gridded, lap clock dead, discardedUnknown, because a discarded session's packets are deleted with it. Possibly drift zones, which the recorder has never seen. The "keep discarded" setting exists to capture such a session onceopen
F8Event types are counted togetherRace, Rivals and others share one best per route and classThe event-type guess rests on two signals and one capture: a position worse than 1st means a race; a lap-clock reset at the start line means Rivals; otherwise unknownopen By decision, until more modes are captured
F9"Valid" is narrower than the game's ruleNo recorded case yetCutting a corner or leaving the track sets no flag. Only rewind and contact are detectedopen
F10Shared replay links can breakAfter a reprocess, a session's five laps had new idsA reprocess deletes and recreates lap rows. Replay and compare links carry lap idsopen
F12A very quick restart is not a separate attemptNo recorded case yetA session is split when the race clock jumps back by more than 10 s. A restart sooner than that stays in the same session, so it counts once. A rewind near the start looks similaropen Needs a capture of one
F13Given-up attempts at events with no lap clock are not countedNo recorded case yetWith no grid position and no lap fields, an abandoned run is indistinguishable from free roamopen
F11Packets can be forgedNone seenUDP has no login. Whoever sends to a driver's port is that driveraccepted

How F3 and F4 produced a fake route

Driver starts a sprint from the gridodometer −70.8, race position 8
drives part of the route, then stops or quits
Stream stops while rollingrung 5 fired: timed to the last packet, flag cutoff
Odometer resets mid-routerung 2 fired: timed as a confirmed finish
the run has a time, so it is fingerprinted
Length 2483 or 4080 against the real course's 5950no route within 5%; no catalogue entry within 5%
no match
New "Unnamed route" rowone per distinct abandoned length

13. What might be fixed

  1. Done: require the distance before calling it a finish (F3, F4). A gridded run is timed only when the odometer has reached the full-route reading (98.5% of 5950), in rungs 2, 3 and 5. A run that stops short keeps its packets but gets no time and no route, and a session with no timed run is discarded like any abandoned event. Old recordings change only when they are reprocessed. Still open: the 5950 figure is measured, not documented, and the test is not applied to events with no lap clock, whose odometer at a finish has not been measured.
  2. Settle the clock origin (F5). Compare one result-screen time with the recording. If the game counts from the grid, time runs from the first packet where the race clock moves (or speed leaves zero) and reprocess the sprints. The replay's alignment follows the same clock.
  3. Name what the catalogue lacks (F6). Rename the route on the Analysis page, and add the entry to the catalogue so a fresh install recognises it.
  4. Capture each event type once (F7, F8). With "keep discarded" switched on, one drift event, one Rivals run and one race on the same route give the data needed to tell them apart, or to show that they cannot be told apart from telemetry.
  5. Approximate off-track (F9). The packet's driving-line field saturates at ±127 when the car is far from the course. A sustained saturation could set an "off course" flag. It would be a second approximation of the game's rule, not the rule.
  6. Give laps a stable identity (F10). Address a lap by session and time anchor, as manual edits already are, so links survive a reprocess.
  7. Statistics from packets. Top speed, air time and time on throttle need a pass over the stored packets, cached per lap.

14. Thresholds

Every number above is a constant in the code, calibrated on real recordings.

ConstantValueMeaning
RACE_OFF_GRACE15 sRace-off or silence before a session ends
RACE_TIME_RESET_JUMP10 sBackwards jump of the race clock that splits a session
RT_FREEZE_SECONDS1.5 sFrozen race clock that marks a finish
FINISH_MIN_RT5 sFreezes and resets before this are ignored
PTP_MIN_RUN_TIME10 sShortest run that is believed
WTA_MIN_LAP_DIST500Odometer units a run or geometric lap must cover
WTA_DIST_COLLAPSE1000Single-frame odometer drop that counts as a reset
POINT_FINISH_DIST200How close to zero the reset must land
WTA_ARM_DIST / WTA_CROSS_DIST120 m / 60 mLeave this far, then return this close, for a geometric lap
WTA_TELEPORT_JUMP250 mSingle-frame move that means fast travel
FINAL_LAP_DIST_FRACTION0.97Share of a typical lap that makes a cut-off lap the final lap
REWIND_TIME_JUMP0.5 sLap clock running backwards
IMPACT_ACCEL / IMPACT_JERK / IMPACT_PEAK45 / 30 / 98 m/s²Contact: threshold, one-frame rise, stand-alone peak
ROUTE_START_RADIUS_M120 mRoute match: start position
ROUTE_LENGTH_TOLERANCE5%Route match: odometer length
ROUTE_SPAN_TOLERANCE15%Route match: width and depth covered (50 m floor)
FULL_ROUTE_UNITS5950Odometer reading at the end of any completed route; a gridded run, and any cutoff time, needs 98.5% of it
AUTO_GROUP_GAP_S2 hLongest gap between attempts in one sitting

Lap and finish detection: app/recorder/laps.py. Storage, routes and sittings: app/recorder/store.py. Best-lap rule: app/rivalry.py. Catalogue: app/tracks.py. Event-type guess: app/eventtype.py.

15. Thanks

SplitTime is built on LapScope by Erdem Darcan. The packet parser, the recorder, the lap and finish detection described above, the route fingerprint, the track catalogue and the Live and Analysis pages are his work, tuned against a great many real captures. SplitTime adds a second driver, the Rivalry, Stats and Replay pages and the rules for comparing drivers. Thank you, Erdem. He announced LapScope in this post on r/ForzaHorizon.

Corner detection is adapted from Forza-6-telemetry by ClickClickMedia. Both projects are MIT-licensed. The faults listed on this page are about SplitTime as it runs here; several come from event types LapScope had not yet seen.