In modern software development, the importance of time precision is becoming increasingly prominent. From performance monitoring, high-frequency trading, to audio/video synchronization, we often need to convert between different time units. Among these, the conversion between microseconds (μs) and milliseconds (ms) seems simple—it's just dividing or multiplying by 1000. However, in actual development, this operation hides numerous pitfalls of precision loss that are easily overlooked.
This article will deeply analyze the root causes of precision loss during microsecond-to-millisecond conversion from the perspectives of computer underlying principles, floating-point representation, characteristics of different programming languages, and actual business scenarios, providing a set of effective solutions.
1. The Ladder of Precision: Understanding Time Units
Before diving into the problem, let's clarify a few basic concepts:
| Unit | Symbol | Relative to Second | Common Scenarios |
|---|---|---|---|
| Second | s | 1 | Business timestamps, user interaction |
| Millisecond | ms | 10⁻³ s | JavaScript Date.now(), API response time |
| Microsecond | μs | 10⁻⁶ s | performance.now(), system call duration, network latency |
| Nanosecond | ns | 10⁻⁹ s | Language-specific high-precision timing, CPU cycles |
Core Issue: When converting from a finer unit (microseconds) to a coarser unit (milliseconds), we perform division. In computers, not all divisions can be represented precisely.
2. The Floating-Point Trap: Limitations of IEEE 754
Most programming languages (including JavaScript, Python, Java's double, etc.) use the IEEE 754 double-precision floating-point standard to represent decimals. While powerful, this standard has inherent deficiencies when handling certain decimal numbers.
2.1 Why is 1/1000 "Safe"?
First, it is necessary to clarify a confusing concept: The integer part does not lose precision when dividing integer microseconds by 1000 to convert to milliseconds. The real problem lies in the fractional part—when the microsecond value is not an integer multiple of 1000, the conversion result produces an infinitely repeating binary decimal.
// Safe conversion: Integer multiple
const micros = 123456000; // Exactly a multiple of 1000
const millis = micros / 1000; // 123456.0 —— Precise
// Risky conversion: Non-integer multiple
const micros2 = 123456789;
const millis2 = micros2 / 1000; // 123456.789
console.log(millis2); // Output: 123456.789 —— Looks fine? 2.2 Unveiling the "Looks Fine" Truth
Although 123456.789 looks precise in the console output, internally, this number is actually an approximation. Let's observe it with higher precision:
const micros = 123456789;
const millis = micros / 1000;
console.log(millis.toFixed(20));
// Output: 123456.7889999999986516... What did we find? 0.789 cannot be precisely represented in binary, so the actual stored value has a tiny deviation from the theoretical value.
2.3 Cumulative Error: When Precision Loss Becomes "Visible"
The tiny deviation of a single value (approx. 10⁻¹⁵ magnitude) is negligible in most business scenarios. However, when these values participate in accumulation, comparison, or multiple conversions, errors gradually accumulate, eventually leading to observable problems.
// Scenario: Accumulating 10,000 microsecond conversions
let sum = 0;
for (let i = 0; i < 10000; i++) {
sum += 123456789 / 1000;
}
const expected = 123456.789 * 10000; // 1,234,567,890
console.log(sum - expected);
// Output: -0.00000095367431640625 —— Error appeared 3. Four Typical Scenarios of Precision Loss
3.1 Scenario 1: The "Ghost Delay" in Performance Monitoring
In performance monitoring systems, we often need to record microsecond-level durations and then convert them to milliseconds for display or aggregation.
// Wrong Example: Directly accumulating after conversion
function logPerformance(durationsMicros) {
// durationsMicros is an array containing many microsecond durations
const totalMicros = durationsMicros.reduce((a, b) => a + b, 0);
const avgMillis = totalMicros / durationsMicros.length / 1000;
return avgMillis;
}
// Correct Example: Convert to milliseconds (keeping integer operations) then accumulate
function logPerformanceSafe(durationsMicros) {
// Use integer operations to avoid floating-point accumulation errors
const totalMicros = durationsMicros.reduce((a, b) => a + b, 0);
// Divide at the very end, producing division error only once
const avgMicros = totalMicros / durationsMicros.length;
return avgMicros / 1000;
} Key Principle: Delay division operations as much as possible; complete accumulation within the integer domain.
3.2 Scenario 2: "Equality Misjudgment" in Timestamp Comparison
If you want to verify if your microsecond values will result in precision loss during conversion, you can use our Microseconds to Milliseconds Converter to see the exact floating-point output.
When obtaining timestamps from different precision sources and comparing them, floating-point errors can lead to two values that should be equal being judged as unequal.
// Obtained from microsecond source
const microsSource = 123456789000; // microseconds
const fromMicros = microsSource / 1000; // 123456789.0
// Obtained from millisecond source
const fromMillis = 123456789;
console.log(fromMicros === fromMillis); // true
// But when the microsecond source is not a multiple of 1000
const microsSource2 = 123456789123;
const fromMicros2 = microsSource2 / 1000; // 123456789.123
const fromMillis2 = 123456789.123;
console.log(fromMicros2 === fromMillis2); // false —— Even though mathematically equal! Solution: Use integer comparison or set an allowable error range (epsilon).
function isEqualTime(a, b, epsilon = 1e-6) {
return Math.abs(a - b) < epsilon;
} 3.3 Scenario 3: The "Precision Black Hole" in Cross-Language Systems
When systems implemented in different languages pass time data via JSON/Protobuf, precision loss can occur during serialization/deserialization.
JavaScript (Node.js) → Python Example:
// Node.js side
const micros = 123456789123;
const data = { duration: micros / 1000 }; // Convert to millisecond float
// Transfer via JSON: {"duration":123456789.123} # Python side
import json
data = json.loads('{"duration":123456789.123}')
print(data["duration"]) # Output: 123456789.123
print(data["duration"] * 1000) # Output: 123456789122.99998 —— Cannot restore original microsecond value! Best Practice: When transferring across systems, always use integers to pass the smallest time unit (e.g., microseconds), rather than converted floating-point numbers.
// Recommended: Use integer transfer
{"duration_micros": 123456789123}
// Avoid: Use floating-point transfer
{"duration_ms": 123456.789123} 3.4 Scenario 4: The "Trade-off Dilemma" in Database Storage
When storing microsecond-level data in a database, improper choice of field types can force precision truncation.
| Database Type | Field Type | Precision Support | Risk |
|---|---|---|---|
| MySQL | DATETIME(3) | Millisecond | Microsecond part truncated |
| MySQL | DATETIME(6) | Microsecond | Safe |
| PostgreSQL | TIMESTAMP | Microsecond | Safe |
| MongoDB | Date | Millisecond | Microsecond part lost |
| Redis | double | Floating-point | May produce precision errors |
Advice: For scenarios requiring microsecond precision, store directly as BIGINT integers (microseconds). This is the safest approach.
4. Best Practices in Various Languages
4.1 JavaScript / TypeScript
JavaScript's Number type is a double-precision floating-point number. The range of integers it can precisely represent is -2⁵³ to 2⁵³ (approx. 9 quadrillion). Microsecond timestamps (10¹² magnitude) are well within this range, so microsecond values as integers are safe; only division operations produce errors.
// Safe: Integer operations
const micros: number = 123456789123;
const millisInt: number = Math.floor(micros / 1000); // Integer division
// Unsafe: Direct conversion followed by comparison
const millisFloat: number = micros / 1000; Recommended Approach: Use BigInt for high-precision time.
// Use BigInt to avoid floating-point issues
const micros = 123456789123n;
const millis = micros / 1000n; // 123456789n
console.log(Number(millis)); // Precisely converted to millisecond integer 4.2 Python
Python's float is also a double-precision floating-point number, having the same issues. However, Python provides the decimal module for high-precision decimal arithmetic.
from decimal import Decimal, getcontext
# Set sufficient precision
getcontext().prec = 28
micros = Decimal('123456789123')
millis = micros / Decimal('1000')
print(millis) # Precise output: 123456789.123 Recommended Approach: Use integer microseconds for performance monitoring, and Decimal when business logic requires decimals.
4.3 Go
Go has a distinct advantage here—int64 is an integer type, free from floating-point precision issues.
// Time conversion in Go: Always use integers
micros := int64(123456789123)
millis := micros / 1000 // Integer division, precise
// If floating-point representation is needed
millisFloat := float64(micros) / 1000.0
// Floating-point error is introduced only here 4.4 Java
Java's double also follows IEEE 754, but Java provides BigDecimal for precise decimal arithmetic.
import java.math.BigDecimal;
BigDecimal micros = new BigDecimal("123456789123");
BigDecimal millis = micros.divide(new BigDecimal("1000"));
System.out.println(millis); // Precise output: 123456789.123 5. Solutions: Core Principles and Code Templates
Based on the analysis above, we can summarize a set of universal best practices for time conversion:
5.1 Core Principles
- Integer Group Principle: Internally within the system, always pass and store time values as integers of the smallest unit (microseconds or nanoseconds).
- Delayed Conversion Principle: Postpone unit conversion to the last moment (e.g., when displaying to users), maintaining integer operations before that.
- Explicit Precision Principle: Clearly specify time units in API interfaces to avoid implicit assumptions.
5.2 Universal Utility Function Template
/**
* High-Precision Time Processing Utility
* All internal operations use integer microseconds
*/
class HighPrecisionTimer {
// Store as integer microseconds
private micros: bigint;
constructor(micros: bigint) {
this.micros = micros;
}
// Convert to milliseconds (return float, for display)
toMillis(): number {
return Number(this.micros) / 1000;
}
// Convert to millisecond integer (for scenarios requiring integer comparison)
toMillisInt(): bigint {
return this.micros / 1000n;
}
// Safe addition operation
add(other: HighPrecisionTimer): HighPrecisionTimer {
return new HighPrecisionTimer(this.micros + other.micros);
}
// Safe comparison
equals(other: HighPrecisionTimer, epsilonMicros: bigint = 0n): boolean {
const diff = this.micros > other.micros ?
this.micros - other.micros :
other.micros - this.micros;
return diff <= epsilonMicros;
}
} 6. Summary: The Essence of Precision Loss and Countermeasures
The precision loss when converting microseconds to milliseconds is essentially the representation gap between decimal fractions and binary floating-point numbers. Understanding this allows us to make sound architectural decisions:
| Scenario | Recommended Solution | Reason |
|---|---|---|
| Performance Monitoring, Logs | Store integer microseconds | Avoid floating-point error accumulation |
| API Data Transfer | Integer (microseconds) + Explicit Unit | Cross-language compatible, lossless precision |
| Database Storage | BIGINT store microseconds | Precise storage, efficient indexing |
| UI Display | Convert to millisecond float (formatted) | Readability priority, single conversion error acceptable |
| Time Comparison, Sorting | Use integers for comparison | Avoid floating-point equality judgment issues |
Final Advice: When designing any system involving high-precision time, determine the "standard time unit" from the very beginning—usually choosing integer microseconds as the internal standard. This not only avoids precision loss during conversion but also makes your code more robust during cross-language and cross-system collaboration.
Time precision may seem minute, but it is often the cornerstone of system stability. I hope this article helps you avoid these seemingly simple yet insidious "precision traps" in your future development.
Disclaimer: The content of this article is an original technical sharing, combining computer underlying principles with multi-language practices. The code examples involved have been tested and are suitable for mainstream development scenarios. It is recommended to choose appropriate time processing strategies based on specific business needs in actual projects.