DEV Community

Cover image for Android elapsedRealtime and Monotonic Deadlines
Raylabs
Raylabs

Posted on Originally published at raylabs.app

Android elapsedRealtime and Monotonic Deadlines

When an Android application starts up, background initialization tasks often race against strict latency budgets. If a network call or dependency check exceeds its allowed time, a naive implementation might still let the delayed response overwrite the user interface state after a timeout has already elapsed. Handling a cold starting android backend requires robust timeout controls that go beyond simple wall-clock checks, ensuring that slow responses never corrupt your application startup sequence.

Quick solution:

suspend fun executeStartupCheckWithDeadline(timeoutMs: Long): StartupResult = withTimeout(timeoutMs) {
    val startTime = SystemClock.elapsedRealtime()
    val remainingTime = timeoutMs - (SystemClock.elapsedRealtime() - startTime)
    if (remainingTime <= 0) throw TimeoutCancellationException("Deadline reached")

    // Perform underlying operation with remaining budget
    fetchRemoteReadiness(remainingTime)
}
Enter fullscreen mode Exit fullscreen mode

Prerequisites: Ensure you handle CancellationException properly and do not swallow it in generic catch blocks, as Kotlin coroutines rely on it for cooperative cancellation.

Which Android clock includes device sleep?

Choosing the right time source is the first line of defense in building reliable startup health checks. System clocks that rely on wall-clock time, such as System.currentTimeMillis(), are vulnerable to sudden jumps caused by user manual adjustments, time zone changes, or automatic network time updates. If a device goes to sleep, wall-clock time may pause or jump unpredictably depending on the underlying OS implementation and hardware state.

To measure elapsed durations accurately, developers must use a monotonic clock. According to the official Android SystemClock reference, methods like SystemClock.elapsedRealtime() and SystemClock.elapsedRealtimeNanos() include the time that the device spent in deep sleep. This guarantees that your startup duration budget decreases at a steady, predictable rate regardless of whether the CPU enters a low-power sleep state while waiting for slow I/O operations.

Can a result arriving after the deadline update UI state?

Even when you wrap operations in a timeout block, asynchronous tasks running on background threads can sometimes deliver results just as the timeout triggers or immediately afterward. If your UI layer accepts these late payloads, you risk introducing race conditions where an expired, failed startup attempt overwrites a newer, successful retry or an explicit error screen.

As detailed in the Kotlin coroutine cancellation and timeouts documentation, cooperative cancellation ensures that when a timeout occurs, the active coroutine receives a cancellation signal. However, if your network library or asynchronous wrapper catches exceptions internally without rethrowing CancellationException, the background task may continue executing and attempt to dispatch its result to the main thread.

To prevent late successes from overriding timeouts, structure your timeout boundaries so that result emission is tightly coupled to the active coroutine job lifecycle. If the job is cancelled or its parent scope completes, any pending callback or LiveData/StateFlow emission must be safely discarded.

How should a newer retry invalidate an older result?

When initialization fails or times out, applications often trigger an automatic retry. If the first attempt was delayed rather than fully terminated, both the original attempt and the newer retry can try to publish their findings to the application state manager. Without proper synchronization, an older, slower response can arrive after a newer retry has already succeeded, degrading the startup health check.

To solve this, implement a request generation token or an atomic sequence counter. Each time a new startup check initiates, increment the generation counter and store it locally. When a background task finishes and prepares to update the application state, it must verify its generation token against the current active token. If the response carries an outdated token, the state manager rejects it immediately.

Approach When to use it Trade-off or failure mode Recommendation
withTimeout block Simple bounded coroutine calls Swallowed exceptions can leak work Use with cooperative suspension points
Monotonic deadline check Manual time budgeting across steps Requires explicit math on remaining time Combine with elapsedRealtime for sleep tracking
Generation token Preventing out-of-order retries Adds state management overhead Essential for overlapping startup retries

Verification and execution checklist

  1. Verify that wall-clock time adjustments on the device do not extend or shorten the active startup duration budget.
  2. Confirm that when a timeout triggers, late-arriving network responses are successfully intercepted and prevented from updating the UI state.
  3. Test overlapping retries to ensure that an older, delayed attempt cannot overwrite the results of a newer startup sequence.
  4. Ensure that cancellation exceptions propagate correctly through your repository and view model layers without being swallowed by broad catch blocks.

Top comments (0)