Sharp Edges GAME SECURITY NOTES

Why Android Games Stutter at High FPS


Your overlay reads 90 FPS. Players say the game feels choppy. The usual next move is to optimize further and push the number higher, which only helps if that number was measuring the thing that is wrong.

Android's own documentation says it often is not. On the page describing how to measure game frame rates, Google writes that the average calculation means intermittent frame drops and micro-stuttering cannot be detected, which makes average FPS insufficient to represent the player experience on its own.

Frame rate and smoothness are two different properties. An average describes speed; stutter is a property of rhythm, and an average carries no information about rhythm.

That is not an argument against FPS as a metric. It works well as a first-layer signal — a game averaging 22 FPS has a problem the average shows you immediately. The trouble starts when the average looks fine and the game still does not.

What "Smooth" Actually Means

A display refreshes on a fixed cadence. What the player experiences is built from the intervals between frames appearing on screen, not from how many appeared in the last second.

Consider two games that both report 60 FPS. One presents a frame every 16.7ms; the other alternates between 8ms and 25ms. The average is identical, and Android's frame pacing documentation names the second pattern directly: short frames followed by long frames are perceived by the player as stuttering.

This is why a consistently slower game can be preferable to a faster inconsistent one. A steady 30 FPS never produces the short-then-long pattern; a game swinging between 90 and 40 produces it continuously. Android's documentation makes the same point from the platform side: an unpaced game often has higher peak frame rates but with higher variance of frame time, which significantly affects how performance is perceived by players.

That peak and that variance come from the same place. A game that is GPU bound with no pacing control runs faster whenever GPU load happens to drop — nothing caps it, so it takes whatever the hardware offers that instant and gives it back the instant load returns.

Three Things That Show Up as Stutter

"The game stutters" describes what a player noticed, not a cause. Three different patterns produce that same report, and a build can have more than one of them at the same time.

  • Occasional long frames. Most frames are fine; individual frames take dramatically longer. Garbage collection, asset loading, shader compilation, and scene transitions all produce this shape, and the player sees a hitch every few seconds against an otherwise smooth background.
  • Frame time variance. No single frame is catastrophic, but the intervals never settle. Short frames follow long ones continuously, which is what fluctuating load produces, and also what the absence of any pacing control produces.
  • Presentation pacing. Frames are rendered on time and still reach the screen at uneven intervals, because rendering completion and display presentation are separate events. Render-side counters look healthy while the player sees stutter.

These are directions to investigate, not boxes a build sorts into. A few very long frames and continuously unsettled intervals both widen the same distribution, and presentation-side unevenness can sit underneath either one, so timing data will usually point you at a likely pattern rather than rule the other two out. An average frame rate points nowhere: long frames get diluted by the normal ones around them, variance is independent of the mean, and presentation timing is not what a render-side counter counts.

Why an Unpaced Game Can Stutter While Rendering Faster

Stutter does not always mean the frame rate is too low. A game with no pacing control, submitting each frame as soon as it is ready, can hand frames to the display faster than the display presents them — and those early frames have to wait somewhere.

The display pipeline holds a queue of frames, typically two, and it fills up when the game presents them faster than the display can show them. With no room left in the queue, the rendering thread is blocked by an OpenGL or Vulkan call. Android's documentation pairs this back-pressure with stuttering, and notes that buffer stuffing may also add an extra frame of latency. The condition here is the missing pacing, not the speed: a game that submits on a schedule matched to the display does not stuff the queue this way, however fast it can draw.

The Android Game SDK announcement gives the frame time shapes this produces. A frame that renders substantially faster than expected shortens the presentation of the previous frame, producing something like a 33ms, 16ms, 50ms sequence. A frame that takes too long is held an extra cycle instead, producing something like 50ms, 16ms, 33ms.

Both sequences arrive at the same place: an uneven interval the player perceives as stutter. Rendering too fast and rendering too slowly are not opposite problems with opposite fixes — both are ways of falling out of sync with the display, and only one of them improves when the game runs faster.

Android's documentation strongly recommends that all games be well paced for this reason. The platform also ships an intervention called FPS throttling, part of Game Mode on Android 13 and later, which the documentation describes as helping unpaced games achieve frame pacing from the platform side. A game that declares support for the Game Mode API overrides it — a platform-side fallback for games that have not paced themselves, not a recommendation that every game cap its frame rate.

What the FPS Number Leaves Out

Six things an average frame rate does not carry, all of which matter precisely when the average looks fine. Each one is a separate measurement, and most of them come from a different place than the FPS counter does.

  • Distribution. The same 60 FPS average covers a steady 16.7ms and an alternating 8/25ms. Percentiles carry this information; means discard it.
  • The size and count of outliers. A handful of very long frames barely move an average computed over hundreds of frames, and they are exactly what the player notices.
  • What a missed frame costs. When a new frame is not ready in time, the hardware displays the previous frame again rather than showing the new one slightly late, so the cost is a full refresh period.
  • Whether presentation intervals were even. Render-side completion counts and display-side presentation timing are different measurements.
  • Whether the system granted the refresh rate you asked for. Android 15 sets a default 60Hz refresh rate for games to optimize power consumption, higher rates have to be requested explicitly through the Frame Rate API or Swappy, and the documentation states the system may override that request based on factors like battery level or device temperature. A requested rate is not a measured one.
  • Whether the data covers your frames. More than one Android path reports frame timing, and they do not cover the same surfaces.

The Slow Rendering documentation and its associated metrics describe frames rendered through the View-based UI toolkit, where the visible portion of the app is drawn from Canvas or the View hierarchy — which is not how a game renders. Slow Sessions, the games-only metric, measures session frame rates using data from SurfaceFlinger, based on the interval between frames drawn on surfaces owned by the game, including OpenGL, Vulkan, and Android UI toolkit surfaces.

The practical consequence is not that a game rendering through Vulkan has no data. It is that the data lives on a different page than the one many developers open first, so confirm which path produced a number before reading anything into it.

A Troubleshooting Order

Android gives an order for this, and the table follows it: treat Slow Sessions as a primary radar for severe degradation, then examine average FPS, P90, and P99 to assess how often frame presentation becomes uneven. Each step narrows the question rather than answering it.

Those metrics do not all live in one place. Slow Sessions is a Play Console Android vitals metric, while average FPS, the percentiles, and the frame time distribution come from adb shell dumpsys SurfaceFlinger --timestats, filtered to your game's layer by layerName. Average FPS appears directly in that output; P90 and P99 must be computed from the presentToPresent histogram it contains.

Step What to look at Where What it tells you What it cannot tell you Scope
1 Slow session rate Play Console — Android vitals, Slow Sessions (games only) Whether a significant share of frames miss your target frame time; the documentation's worked examples are more than 25% of frames slower than 50ms (20 FPS) or 34ms (30 FPS), given as illustrations rather than thresholds every game should adopt Which of the three patterns is responsible Games; covers OpenGL, Vulkan, and UI toolkit surfaces
2 Average FPS dumpsys SurfaceFlinger --timestats output, or in-engine stats Whether the game is broadly too slow Variance, outliers, presentation evenness — the documentation states the average cannot detect intermittent drops or micro-stuttering Any renderer
3 P90 frame rate Computed by you from the presentToPresent histogram in that dump Whether the consistent baseline sits close to the average; when it does, the session is running well for the vast majority of frames Rare extreme frames — a 90th percentile is silent about the slowest 10% by construction, which is why P99 is a separate metric Any renderer
4 P99 frame rate Same histogram, same calculation at the 99% threshold The tail: rare long frames the other metrics dilute away Whether the cause is load, collection, or presentation Any renderer
5 Frame time distribution The presentToPresent histogram itself, in the same dump How the intervals are spread, and how far the worst ones sit from the bulk of them The order the frames arrived in, so it cannot show whether long and short frames alternate, whether the long ones cluster, or what the game was doing at the time Any renderer

Steps 1 through 4 narrow severity rather than cause. They tell you how bad the tail is, not what produces it.

Step 5 is where that distinction usually gets misread. A histogram is buckets and counts with the timeline thrown away, so a distribution with a heavy tail is consistent with a few isolated hitches, a burst of them during one scene transition, and a steady long-short alternation — and it reads the same in all three cases. Frame order and per-frame work live in a trace: Perfetto, an engine-side frame timeline, or GPU-side profiling, depending on what you are chasing. Reach for one as soon as you need sequence or cause rather than spread.

If You're on Unity

Everything here is Unity's implementation behaviour, not Android platform behaviour. Other engines choose differently, and none of the mechanisms covered so far depend on any of it.

Application.targetFrameRate documents Android separately, and the Android text matters. QualitySettings.vSyncCount > 0 is ignored there, and in the default configuration — vSyncCount = 0 with targetFrameRate = -1 — content renders at a fixed 30 fps to conserve battery, independent of the display's native refresh rate. A project that never sets targetFrameRate is capped at 30 whatever the panel can do.

The same page documents a rounding rule that accounts for a common confusion. If the requested rate does not evenly divide the display's current refresh rate, targetFrameRate is rounded down to the nearest value that does: Unity's own example is a 60Hz Android display where a request for 25 renders at 20, because 20 is the highest number below 25 that divides 60 evenly. Rendering is capped by the display's maximum refresh rate regardless.

Unity describes Optimized Frame Pacing as letting the engine evenly distribute frames for less variance in framerate. The description puts variance rather than throughput at the centre of what the option addresses.

The documentation for Adaptive Performance's Samsung Android provider states that Adaptive Performance and Variable Refresh Rate are not compatible with frame pacing, and that disabling Optimized Frame Pacing is best practice when using them. A project combining that provider with Optimized Frame Pacing has a configuration conflict to resolve before treating the symptom as a performance problem.

Where This Stops

This gets you to a shortlist and a set of numbers. It does not get you to a cause.

Confirming which metric source covers your renderer tells you whether the numbers you have been reading apply to you at all. The percentile spread tells you how much of the session is affected, and the distribution tells you how far the bad frames sit from the good ones. A cause lives in what happened during one specific long frame, which is why the next step after all of this is a trace.

If average FPS is low, reducing rendering cost may help. If it looks healthy, inspect presentation timing and trace the specific hitch before trying to push FPS higher.

Sources

← Back to list