A player collects a twelve-hour offline reward, closes the game, moves the device clock forward a day, and collects it again. Another player runs the game under a speed tool and burns through a cooldown in a fraction of the time it should take. Both are attacks on time, and they are not the same attack.
The usual answers come in two sizes. One is "use server time," which is correct and already documented by Unity, and which stops being useful the moment the requirement includes working offline. The other is a snippet that flags Time.timeScale > 1.2f as cheating, which treats one engine value as a proxy for a much larger question.
What sits between them is the part nobody lays out: which clocks exist, what each one measures, and which of them a given tampering action actually moves. Those are answerable questions, and the answers are in platform documentation rather than in anyone's detection heuristic.
On Android, "the time" is not one value. There are at least three system clocks measuring different things, and only one of them can be set by the user.
Getting that straight is what makes the offline question answerable at all. The answer depends entirely on which differences stay comparable when the wall clock moves, and that varies by clock.
Android Exposes Three Clocks
The Android SystemClock documentation draws the distinction directly. The three clocks are not interchangeable, and the documentation says which one belongs to which job.
System.currentTimeMillis() is the standard wall clock, expressed as milliseconds since the Unix epoch. It can be set by the user or by the phone network, so it can jump forwards or backwards unpredictably. The documentation is explicit that this clock should be used only when correspondence to real-world dates and times is required, and that interval or elapsed time measurement should use one of the others.
SystemClock.uptimeMillis() counts milliseconds since boot. It stops counting when the system enters deep sleep, but it is unaffected by clock scaling, idle, or other power-saving mechanisms. It is the basis for Thread.sleep, Object.wait, System.nanoTime and Handler, and it is guaranteed to be monotonic.
SystemClock.elapsedRealtime() also counts since boot, and unlike uptimeMillis() it includes time spent in deep sleep. The documentation recommends it as the basis for general-purpose interval timing.
The practical shape of that is worth stating plainly. A user who opens Settings and changes the date is changing currentTimeMillis(). The two boot-based clocks are not exposed to that control at all, which is a different thing from saying they are secure — they answer a different question, and the question they answer is "how much time has passed" rather than "what time is it."
What Unity's Time API Says About Itself
Unity's own timing properties are documented against engine behaviour rather than against Android internals. That distinction matters for what can be claimed about them, and it is why the two sets of documentation have to be read separately before they are read together.
Time.realtimeSinceStartupAsDouble is the property the documentation points at for measuring real time. Three statements in it are directly relevant. Time.timeScale does not affect the property. It returns time as reported by the system timer. And on desktop, web, XR, and mobile platforms, the returned value includes time that elapsed while the system was in sleep mode — with the note that console platforms differ.
Time.time is the scaled clock: it advances with timeScale, which is why a speed tool that changes timeScale moves it. Time.unscaledTime is the per-frame equivalent that timeScale does not affect, and unlike realtimeSinceStartup it returns the same value throughout a frame.
The same documentation records two limits that matter more than they first appear. The property is not constant if called multiple times within a frame, and depending on platform and hardware it may report the same time across several consecutive frames — so a difference between two readings can come out as zero. There is also a known issue where realtimeSinceStartup and realtimeSinceStartupAsDouble return incorrect values when used in OnDestroy.
Read together, those two say something that gets lost in most discussions of this topic. Picking the right clock is not the end of the problem, because when and where you read it is a variable in its own right.
What a Single Tampering Action Actually Moves
The table below holds only what the documentation states or directly entails. Cells where neither applies are marked as not addressed rather than filled in by inference, and the reasoning that would fill the important one follows underneath.
| User changes system clock | Time.timeScale changed |
Device reboots | Deep sleep | |
|---|---|---|---|---|
System.currentTimeMillis() |
Jumps (settable) | Not addressed | Not addressed | Keeps counting |
SystemClock.uptimeMillis() |
Not addressed | Not addressed | Resets (since boot) | Stops counting |
SystemClock.elapsedRealtime() |
Not addressed | Not addressed | Resets (since boot) | Keeps counting |
Time.time |
Not addressed | Scales | Resets (since app start) | Not addressed |
Time.unscaledTime |
Not addressed | Unaffected | Resets (since app start) | Not addressed |
Time.realtimeSinceStartupAsDouble |
Not addressed | Unaffected | Resets (since app start) | Keeps counting |
The cell worth pausing on is realtimeSinceStartupAsDouble under a system clock change. Unity's documentation does not state what happens to that property when the system clock moves. That silence is itself useful information, and it is where the only inference in this table would go.
Here is what can reasonably be drawn from the two documents, with the strength it actually carries. Unity says the property is unaffected by timeScale and includes system sleep time. Android says elapsedRealtime() includes deep sleep and that the wall clock is the settable one. Those two descriptions agree on both of the dimensions they overlap on, which means that on those two dimensions the same reasoning applies to both. What does not follow is that one is implemented in terms of the other. Unity has not committed to an implementation, so that agreement should not be extended to dimensions neither document covers, and should not be assumed to hold across engine versions or device models.
That is a weaker claim than "realtimeSinceStartup maps to elapsedRealtime()." It is also the claim the documentation actually supports, and the difference between the two matters as soon as an engine version changes.
Why the Monotonic Clocks Are Not the Answer
At this point the boot-based clocks look like the way out. They are not moved by the settings app, not moved by timeScale, and elapsedRealtime() even keeps counting through deep sleep. A cooldown anchored to one of them appears to survive everything discussed so far.
It does not survive a reboot, and this is not an inference — it is what the definition says. Both uptimeMillis() and elapsedRealtime() count since boot. When the device restarts, the counter restarts from zero.
The consequence for an offline cooldown is total rather than partial. A value stored before a reboot and compared against a reading taken after it is being compared against a counter that started over, so the comparison does not mean anything. There is no adjustment that recovers the lost reference, because the information needed to make the adjustment — how long the device was off — is not in either counter.
This is why the boot-based clocks cannot be the whole answer for anything that has to survive a device reboot. They can remain meaningful across app restarts within the same boot if the earlier reading is persisted, but once the device restarts, that reference is lost.
Where the Tampering Happens Decides Whether Comparing Helps
Everything above assumes tampering occurs at a level the application can see — a user changing the date in Settings, or an engine value being altered. Under that assumption the clocks diverge from one another, and a divergence is something an application can observe.
The following is the least certain claim in this article, and it is a structural argument rather than something either document states. If a tampering action operates below the level of these APIs — altering a source they all derive from — then comparing two values that come from that same source would not produce an independent signal. Two readings drawn from one underlying source agree with each other whether or not that source is accurate. The independence that makes a comparison meaningful comes from the values having separate origins, not from their having separate names. This is the same boundary seen from inside the process, where the code doing the checking and the code being checked sit on one side of it together.
This is worth understanding as a limit on the method rather than as a reason to abandon it. Comparing clocks is informative exactly to the extent that the clocks being compared are actually independent, and how independent they are is not something the API surface tells you.
What Remains Comparable Offline
So what is left, for a game that has to hold a cooldown while disconnected? The honest answer is a set of differences rather than a single trustworthy value, which is less satisfying than a single API name and more useful. Within one device boot, elapsedRealtime() can provide a continuous interval even across app restarts and deep sleep. Once the device reboots, that monotonic reference is lost. The wall clock remains available across boots, but it is also the clock the user can change.
That gap does not close with a cleverer choice of clock. It closes, when it needs to close, by moving the decision somewhere the client does not control, which is where the four responsibilities a game has to place actually sit. The point for this article is narrower: knowing which clock measures what is what lets you tell which decisions genuinely need that move and which ones do not.
Some do not. A cosmetic daily-login streak that a determined player can inflate costs nothing to get wrong. A cooldown on a consumable that only affects the player's own pacing is in the same category. Others — anything feeding a shared economy, a leaderboard, or a purchase — are in a different one entirely, and no arrangement of client-side clocks changes that.
The Question to Ask First
The useful starting point is not which clock to use. It is what tolerance each individual decision has.
For any time-gated thing in the game, two questions do most of the work. How wrong can this be before it matters — minutes, hours, or not at all? And what should happen during the window where the device is offline and the answer cannot be confirmed — does the game grant optimistically and reconcile later, hold the reward until it can be confirmed, or treat the gate as advisory?
Those questions have different answers for a daily login streak and for a timed resource that converts into currency, which is precisely why a single clock choice applied across the whole game tends to be wrong somewhere. The clocks are the vocabulary for answering them, not a substitute for asking.