Today I opened two of my swims in Garmin Connect next to the same swims on the Vimo Swim site and compared every field. The good news first: all 35 laps of a pool swim matched on distance, time, SWOLF, heart rate and strokes. The bad news came from the records page. My "fastest pace" was 0:06 per 100 m, from a swim in November 2022.
That swim says 2.63 km in a session of under nine minutes. One lap reads 2.5 km in 61 seconds, with 100 lengths in it. Nobody swims that. The watch miscounted, and Garmin kept the result as it was.

What else the comparison found
| Field | Garmin Connect | Our page | Why |
|---|---|---|---|
| Open water pace | 2:13 /100 m | 2:28 /100 m | we divided by elapsed time, Garmin by the timer |
| Pool lap pace | 2:02 | 2:01 | rounding, in 3 of 35 laps |
| Fastest pace record | best 400 m 3:07 | 0:06 /100 m | one broken swim wins every record |
| Activity title | a real place name | "Pool Swim" | the summary we stored had the generic name |
The pace fix was one line: use the swimming time when the pool lengths give it, else the watch timer, else elapsed time. The records needed more than a fix. They needed a way to know which swims to ignore.
The check
I did not want to fix the data. A swimmer should see what their watch recorded. So the check only writes notes next to the swim, and the records skip any swim that has notes.
flowchart LR
A[FIT file] --> B["Read laps<br/>and lengths"]
B --> C{"Faster than<br/>2 m/s?"}
C -->|yes| D[Note on the swim]
C -->|no| E[Best times]
D --> F["Banner, filter,<br/>records skip it"]
The rules are plain numbers. A pool length faster than 2 m/s (12.5 s per 25 m, elite sprint speed with a push-off), a length longer than 15 minutes, a lap faster than 2 m/s, or a swimming time longer than the whole session.
Tuning the limit
The first version used 2.5 m/s, just above the world record. It still let a 100 m best of 0:43 through, close to world class, so I lowered it to 2 m/s. The 100 m best moved to 1:02, which I believe. Then my longest swim, a real 10 km from 2023, disappeared from the records. One length of 12.2 seconds had flagged the whole swim.
| Rule | Swims flagged (of 1,036) | Best 100 m |
|---|---|---|
| No check | 0 | 0:00.9 |
| Over 2.5 m/s | 43 | 0:43 |
| Over 2 m/s, any length | 111 | 1:02 |
| Over 2 m/s, 3+ lengths | 67 | 1:02 |
One quick length is a missed turn. Three or more means the watch was lost. A single bad length still never makes a best time, because it breaks the run of lengths the best times are built from.
The check runs when the FIT file is read, so old swims need a second read. I added a --reread flag to the backfill command. It read all 1,036 stored files again, with no errors.
The site now shows a yellow box on a flagged swim with the reasons, a "Check data" badge in the list, and a filter that lists only the flagged swims, so I can go through them one by one.
It took an afternoon, most of it spent tuning, not coding. Next time I would start from the false positives: pick a few swims I know are real, make sure they pass, and only then lower the limit.

Related notes
From the first Garmin swim to a details page in one evening
Garmin Connect started pushing swims to Vimo Swim on October 29. The summary showed up on the site within minutes, the laps and heart rate took one more evening. What made it quick: store every push raw before you parse anything.
Getting paid in USD as a Polish sole trader: Wise, wFirma and one morning
I moved a US client from my limited company to my own sole trader business in one morning. Wise verification, US account details on a Polish invoice, and the wFirma traps I hit on the way.



