Topics
Recent articles

Android & Mobile

Android elapsedRealtime and Monotonic Deadlines

Learn how to use Android elapsedRealtime and Kotlin cancellation to reject late coroutine responses during startup health checks, preventing stale data from overwriting UI state.

Table of Contents5 sections
Teen programming with multiple laptops in a modern workspace. Ideal for tech and education themes.
Teen programming with multiple laptops in a modern workspace. Ideal for tech and education themes.

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)
}

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.

Continue Exploring

You Might Also Like

View all articles
Fix Firebase Variant CI Configuration
3 min read

Fix Firebase Variant CI Configuration

Learn how the Google Services Gradle plugin matches build variants to configuration files, where variant-specific JSON files live, and how to verify that your CI pipeline exercises the intended Firebase project.