Stop Mixing Milliseconds and Seconds with F# Units of Measure
F# units of measure catch mismatched time, length, and quantity units at compile time with zero runtime overhead.
15 Jul 2025, 09:52 UTC

Stop Letting Milliseconds Masquerade as Seconds
A function requests a timeout in seconds. A caller passes a value measured in milliseconds. In many languages both are just `int` or `float`, so the code compiles and the service waits far longer than intended—or fails almost immediately. This class of bug is especially common in distributed systems, where one service configures a retry interval in milliseconds while another reads it as seconds. The useful takeaway is that F# units of measure attach unit names to numeric types at compile time, catching this mismatch before deployment with no runtime cost.
What Units of Measure Are
Units of measure are compile-time labels for numeric values. You declare a unit such as `ms` or `seconds` with the `[<Measure>]` attribute, then annotate values like `1000.0<ms>`. The F# compiler tracks these labels through arithmetic, function arguments, and return types. If you add `5.0<seconds>` to `1000.0<ms>`, the compiler rejects the operation because the units do not match. Because the checks happen during compilation, there is no need for runtime unit tests solely to verify unit consistency. When the program compiles to IL, the annotations are erased, so there is no performance penalty at runtime.
Worked Example: Safe Timeout Configuration
Suppose you are configuring an HTTP client timeout. Define the units and a function that accepts only seconds:
[<Measure>]
type ms
[<Measure>]
type seconds
let configureTimeout (timeout: float<seconds>) =
printfn "Timeout set to %f seconds" timeout
The compiler rejects a plain number:
configureTimeout 5.0
// Compilation error: expected float<seconds>, got float
It also rejects milliseconds:
let delay = 5000.0<ms>
configureTimeout delay
// Compilation error: unit mismatch between seconds and ms
You must convert explicitly:
let toSeconds (value: float<ms>) = value / 1000.0<ms/seconds>
configureTimeout (toSeconds delay)
Verification: Run `dotnet build` in the project folder. The build should fail on the mismatched call and succeed after the explicit conversion. No special permissions are required. The only risk is a build failure, not a runtime change.
Trade-off: Safety Inside F#, Not Across Boundaries
Units of measure exist only in F# metadata. When values cross into C# libraries or are serialized to JSON, the unit information disappears. A `float<seconds>` becomes a plain `float` at the .NET boundary, so units cannot validate data arriving from external systems. They also do not protect against logic errors where the right unit is used for the wrong concept, such as using a timeout value as a polling interval. Use them for internal calculations and convert at system edges.
Comparison: Plain Floats vs. Measured Floats
| Approach | Catches unit confusion | Runtime cost | Works across .NET boundary |
|---|---|---|---|
| Plain `float` | No | None | Yes |
| `float<seconds>` | Yes, at compile time | None | No, erased at runtime |
Actionable Step
Pick one numeric domain in your codebase where units are easily confused—timeouts, byte sizes, or distances. Add `[<Measure>]` types, annotate the relevant function parameters, and compile. Fix any unit mismatches before the code reaches production. Start with internal functions rather than public APIs that interact with other .NET languages.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.