Streaming large language model (LLM) responses token-by-token creates a perceptual problem: users expect smooth, typewriter-style text reveal at ~60fps, but network jitter, garbage collection pauses, and system clock adjustments introduce visible stuttering. The root cause is subtle—most mobile UI frameworks schedule animations using wall-clock time (Date.now() or System.currentTimeMillis()), which can jump backward during NTP corrections or forward during CPU throttling. This article explores monotonic clock discipline: a technique that decouples token arrival timestamps from render scheduling, yielding jank-free LLM streaming on iOS, Android, and web.
The Wall-Clock Problem in Token Streaming
When an LLM API streams tokens via Server-Sent Events or chunked HTTP, the client receives them at irregular intervals—some arrive in 12ms bursts, others after 180ms gaps due to model decode latency or network congestion. A naive implementation timestamps each token with Date.now() and animates its appearance using a linear interpolation:
const elapsed = Date.now() - token.arrivalTime; const progress = Math.min(elapsed / FADE_DURATION, 1.0); element.style.opacity = progress;
This breaks when the system clock adjusts mid-stream. On iOS, background NTP synchronization can shift Date.now() by ±200ms. On Android, aggressive doze mode can freeze System.currentTimeMillis() for seconds. The animation either skips frames (opacity jumps from 0.3 to 1.0) or reverses (tokens flicker backward). Users perceive this as lag or glitchiness, undermining the "intelligence" illusion that smooth streaming creates.
Monotonic vs. Wall-Clock Time
A monotonic clock (e.g., performance.now() on web, CACurrentMediaTime() on iOS, SystemClock.elapsedRealtimeNanos() on Android) measures elapsed time since an arbitrary epoch and never jumps backward. It ignores NTP corrections, timezone changes, and daylight saving shifts. For animation scheduling, this is ideal: frame deltas remain consistent regardless of external time adjustments.
The challenge: token arrival timestamps from the network layer use wall-clock time (HTTP headers carry UTC), but the render loop needs monotonic deltas. The solution is a clock discipline layer that translates between the two domains without accumulating drift.
Implementing Clock Discipline
The core idea: maintain a monotonic epoch offset that converts wall-clock token timestamps into the render loop's monotonic timeline. On the first token arrival, snapshot both clocks:
const wallClockEpoch = Date.now(); const monotonicEpoch = performance.now();
For each subsequent token, compute its monotonic arrival time:
const tokenWallTime = parseServerTimestamp(token); const tokenMonotonicTime = monotonicEpoch + (tokenWallTime - wallClockEpoch);
This isolates the render loop from wall-clock jitter. If the system clock jumps +500ms during streaming, tokenMonotonicTime remains smooth because the epoch offset absorbs the delta. However, unbounded drift can accumulate if the stream lasts minutes—wall-clock and monotonic clocks drift at ~50ppm on mobile (±3ms per minute). For sessions under 2 minutes, this is negligible; for longer streams, periodic re-anchoring is required.
Frame Budget Accounting
Monotonic timestamps alone don't guarantee 60fps. The render loop must also budget work per frame. On a 60Hz display, each frame has a 16.67ms budget. If token processing (DOM updates, layout recalc, style application) exceeds this, frames drop. The discipline layer tracks cumulative frame debt:
let frameDebt = 0;
function onAnimationFrame(monotonicNow) {
const frameBudget = 16.67;
const startTime = performance.now();
// Process tokens until budget exhausted
while (pendingTokens.length > 0 && frameDebt < frameBudget) {
const token = pendingTokens.shift();
renderToken(token, monotonicNow);
frameDebt += performance.now() - startTime;
}
frameDebt = Math.max(0, frameDebt - frameBudget);
requestAnimationFrame(onAnimationFrame);
}If a token takes 22ms to render (e.g., triggering a complex CSS animation or WebGL update), the loop defers the next token to the following frame, preventing jank. This frame-budget accounting is critical for devices with variable refresh rates (ProMotion displays) or thermal throttling—monotonic time keeps animations smooth, but budget discipline keeps them consistent.
Platform-Specific Gotchas
iOS: CACurrentMediaTime and Background Suspension
On iOS, CACurrentMediaTime() pauses when the app enters background state. If a user switches to another app mid-stream and returns 30 seconds later, the monotonic clock is frozen, but the server continued emitting tokens. The discipline layer must detect suspension via UIApplication.didEnterBackgroundNotification and re-anchor the epoch on resume:
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification) {
monotonicEpoch = CACurrentMediaTime();
wallClockEpoch = Date().timeIntervalSince1970;
}Without this, the first frame after resume processes all queued tokens in a single burst, causing a visible stutter.
Android: Doze Mode and Alarms
Android's doze mode can delay SystemClock.elapsedRealtime() by up to 15 minutes for background apps. For foreground LLM streaming, this is rare, but WorkManager-based background token processing (e.g., pre-fetching responses) must use AlarmManager.setExactAndAllowWhileIdle() to guarantee timely wakeups. The discipline layer should gate token processing on PowerManager.isInteractive() to avoid wasted work during deep sleep.
Web: performance.now() and Spectre Mitigations
Post-Spectre, browsers reduce performance.now() precision to 100μs (Chrome) or 1ms (Firefox with privacy.resistFingerprinting). For 60fps animation (16.67ms frames), this is acceptable, but sub-millisecond jitter in token arrival becomes invisible. The discipline layer can exploit this: if two tokens arrive within 1ms (below the precision threshold), batch their DOM updates into a single layout pass, reducing reflow overhead by ~30% in testing with React's concurrent renderer.
Real-World Impact: HearingAid Pro Case Study
In HearingAid Pro—an AirPods-based DSP app shipping real-time transcription—streaming LLM summaries of conversations exhibited visible jank on iPhone 12 devices during high CPU load (simultaneous audio processing + transcription). Switching from Date.now() to CACurrentMediaTime() with frame-budget accounting reduced dropped frames from ~8% to 2min drift.
stress-ng) alongside streaming to verify frame-budget discipline holds.requestAnimationFrame deltas exceeding 20ms; alert if >2% of frames drop.When to Skip Monotonic Discipline
For non-streaming UIs—static text rendering, pagination, or batch-loaded content—wall-clock time suffices. The overhead of epoch management (~0.1ms per token) is wasted if animations are CSS-only (GPU-accelerated, immune to clock jitter). Similarly, server-side rendering or static site generation eliminates the problem entirely. Reserve monotonic discipline for interactive, real-time token streams where perceived smoothness directly impacts user trust in the LLM's responsiveness.
Conclusion
Monotonic clock discipline is invisible infrastructure—users never notice it working, only its absence. By decoupling token arrival timestamps from render scheduling and budgeting frame work, mobile LLM UIs achieve the smooth, typewriter-style streaming that makes generative AI feel intelligent rather than janky. The technique costs ~50 lines of platform-specific code but eliminates an entire class of timing bugs that wall-clock naivety introduces. For production LLM products targeting mobile, it's non-negotiable.