Godot stuttering: find the cause and fix it
The Glitchless team · October 27, 2026 · Godot 4.3 and later
"Stuttering" means two different things in a Godot game. Movement that judders while the frame rate looks fine is a timing problem between physics and drawing. A frame that freezes for a moment, often the first time something appears, is a hitch: one frame did too much work. The fixes are different, so find out which one you have before you change anything.
The short answer
- Movement and camera jitter: turn on physics interpolation, and move bodies in
_physics_process() - Shader compilation stutter: update to Godot 4.4 or later, and draw new effects once behind the loading screen
- Hitches when many things spawn at once: pool and reuse nodes, and draw crowds with MultiMesh or GPU particles
- Stutter that gets worse the longer you play: free the nodes you remove; orphan nodes pile up
- A long freeze on a level change: load the next scene in the background with
ResourceLoader.load_threaded_request()
First, find out which stutter you have
Run the game from the editor and open the Debugger panel. The Monitors tab graphs frame time, process and physics time, draw calls, objects and orphan nodes. The Profiler shows script time per function, and the Visual Profiler shows CPU and GPU time for each rendering step.
Then look at the shape. Steady numbers with juddering movement point to physics timing. A spike the first time something appears points to shader compilation. Spikes when things spawn point to node bursts. A line that keeps climbing points to a leak.
On Godot 4.4 and later, the Pipeline Compilations monitors count the shaders compiled while the game runs. If the Draw count goes up at the moment you see a hitch, that hitch is shader compilation.
The editor isn't what your players run, so check an exported build too. Godot smooths delta by default, which can hide a hitch, so time frames from the clock instead:
frame_log.gd (add as an autoload)
# Prints the typical, 95th percentile and worst frame every 5 seconds, in any build,
# with the pipelines compiled while drawing so far (Godot 4.4+). Orphan nodes are only
# counted in debug builds; a release export prints 0.
extends Node
var _frames := PackedFloat32Array()
var _last := Time.get_ticks_usec()
var _total := 0.0
func _process(_delta: float) -> void:
var now := Time.get_ticks_usec()
var ms := (now - _last) / 1000.0
_last = now
_frames.append(ms)
_total += ms
if _total < 5000.0:
return
_frames.sort()
var n := _frames.size()
print("frame ms: typical %.1f, 95th %.1f, worst %.1f | pipeline compiles %d, orphan nodes %d" % [
_frames[int(n * 0.5)], _frames[int(n * 0.95)], _frames[n - 1],
Performance.get_monitor(Performance.PIPELINE_COMPILATIONS_DRAW),
Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)])
_frames.clear()
_total = 0.0
Movement and camera jitter
What you see. The frame rate is fine, but moving objects and the camera judder. It's worst on 120 Hz and 144 Hz screens, and when the camera follows a CharacterBody.
Why. Physics runs at a fixed rate, 60 ticks a second by default, while frames are drawn at the screen's refresh rate. When the two don't line up, some frames show a body in the same place twice and others skip ahead. A camera updated in a different step from the body it follows makes it worse.
Turn on Physics Interpolation in Project Settings (physics/common/physics_interpolation). Godot then draws physics objects between their last two positions, so movement is smooth at any refresh rate. It works for 2D from Godot 4.3 and for 3D from Godot 4.4.
Move bodies in _physics_process(), not _process(). When you set a position directly, for a respawn or a teleport, call reset_physics_interpolation() straight after, or the object glides across the screen to its new spot.
Once interpolation is on, a Camera2D takes care of itself: it ignores its Process Callback and moves with physics. In 3D, have the camera follow where the body is drawn by reading the body's get_global_transform_interpolated() in _process().
A few things aren't interpolated: GPUParticles2D (use CPUParticles2D for particles attached to a moving body) and 3D bodies you create directly through the physics server.
player.gd
# Project Settings > Physics > Common > Physics Interpolation: on
extends CharacterBody2D
const SPEED := 300.0
func _physics_process(_delta: float) -> void:
velocity = Input.get_vector("left", "right", "up", "down") * SPEED
move_and_slide()
func respawn(at: Vector2) -> void:
global_position = at
reset_physics_interpolation() # jump there instead of gliding
Shader compilation stutter
What you see. A freeze of a fraction of a second the first time an effect, enemy, material or particle system appears, and not the second time. It's often worst on a fresh install or after a driver update.
Why. The GPU needs a compiled pipeline for each combination of shader and render settings. Godot builds one the first time it draws that combination, and the frame waits for it.
Godot 4.4 added ubershaders and pipeline precompilation to the Forward+ and Mobile renderers (Vulkan, Direct3D 12 and Metal), which removes most of these hitches with no work on your side. If you're on an older 4.x, updating is the biggest single fix.
The Compatibility renderer (OpenGL and WebGL 2, used by web and many Android exports) doesn't get that. There, and for anything that still compiles mid-game, draw it once behind the loading screen: put each effect, mesh and particle system the level uses in front of the camera for a couple of frames while the loading screen covers the view. Objects outside the camera aren't drawn, so they don't compile.
Don't change settings that rebuild pipelines during play, such as MSAA or shadow quality, and don't switch on new material features mid-level (transparency, emission or a new texture slot on a StandardMaterial3D). Each combination is a different shader.
Godot 4.5's Shader Baker (an export preset option, Forward+ and Mobile only, not web) compiles shaders when you export. It makes the first launch faster, most on Direct3D 12 and Metal, but it doesn't stop hitches during play.
warm_up.gd
# Behind the loading screen: draw each effect once so its shaders compile before play. func warm_up(camera: Camera3D, effects: Array[PackedScene]) -> void: var in_view := camera.global_transform.translated_local(Vector3(0, 0, -4)) var nodes: Array[Node] = [] for scene in effects: var node: Node3D = scene.instantiate() add_child(node) node.global_transform = in_view nodes.append(node) # Two frames, so each one is drawn while the loading screen covers it. await get_tree().process_frame await get_tree().process_frame for node in nodes: node.queue_free()
Hitches when many things spawn at once
What you see. A hitch every time a wave starts, a big explosion goes off or a pile of pickups appears.
Why. Every instantiate() and add_child() costs time: building the node tree, running _ready(), registering collision shapes. A hundred of them in one frame is a dropped frame.
Make what a level needs during loading and keep it in a pool. Hide and disable a node instead of freeing it, then reuse it.
Draw many copies of one mesh with a single MultiMeshInstance3D or MultiMeshInstance2D, and particles with one GPUParticles3D or GPUParticles2D node, instead of a node each.
If you must create a lot, spread it out: ten a frame over ten frames instead of a hundred at once.
pool.gd
# Make bullets while the level loads, then reuse them. extends Node2D var _free: Array[Node2D] = [] func prepare(scene: PackedScene, count: int) -> void: for i in count: var b: Node2D = scene.instantiate() add_child(b) give_back(b) func take() -> Node2D: if _free.is_empty(): return null # or grow the pool, outside a busy frame var b: Node2D = _free.pop_back() b.process_mode = Node.PROCESS_MODE_INHERIT b.show() return b func give_back(b: Node2D) -> void: b.hide() b.process_mode = Node.PROCESS_MODE_DISABLED _free.append(b)
Stutter that gets worse the longer you play
What you see. Smooth at first, then slower and more hitchy after a few levels or restarts, with memory that keeps climbing.
Why. remove_child() takes a node out of the scene tree but doesn't free it. Removed nodes keep their memory and resources, and an array, dictionary or autoload that still points at them keeps them alive.
Call queue_free() on nodes you're done with, or return them to a pool. Clear arrays, dictionaries and autoload variables that hold nodes from the last level.
Watch Orphan Nodes in the Monitors tab while you change levels in a debug run. It should drop back down each time. Node.print_orphan_nodes() lists them (debug builds only).
A long freeze on a level change
What you see. The game freezes for half a second or more when a level starts or a big menu opens.
Why. load() and change_scene_to_file() load everything on the main thread, and nothing can be drawn until they finish.
Request the next scene in the background, keep the loading screen animating, and switch when it's ready. Keep _ready() in the new scene light too, since it runs in the frame the scene enters the tree.
loader.gd
var _path := "" func load_level(path: String) -> void: _path = path ResourceLoader.load_threaded_request(path) func _process(_delta: float) -> void: if _path.is_empty(): return if ResourceLoader.load_threaded_get_status(_path) == ResourceLoader.THREAD_LOAD_LOADED: get_tree().change_scene_to_packed(ResourceLoader.load_threaded_get(_path)) _path = ""
Check the fix
Measure the same way before and after: the same level, the same route, the same kind of build. Compare the typical frame, the 95th percentile and the worst frame from the logger above, not the average FPS, which hides hitches.
A hitch fix should bring the worst frame down. A jitter fix shows in how movement looks, so record a few seconds on the same screen before and after.
Stutter that only your players see
Your machine isn't your players'. A pipeline that compiles in a few milliseconds on your GPU can take far longer on a mid-range phone, and some older Android GPUs can't build a shader at all on the Compatibility renderer: they log errors like exceed GL_MAX_FRAGMENT_UNIFORM_VECTORS and carry on without the effect. Nothing crashes, so nobody tells you.
Glitchless is an open-source addon for Godot 4.5 and later that times every frame of every player's session from the clock, names each hitch's likely cause (a pipeline compile, a scene change, a burst of new nodes) and tracks orphan nodes per level, split by device and version. Your coding agent reads the same data and can check its own fix: start the build with --glitchless-measure=10 and it prints the numbers.
Ask your coding agent
Use Glitchless to find what makes my Godot game stutter for players: list the worst hitches by cause and level, fix the top one, then measure the build with --glitchless-measure=10 to show the difference.
Frequently asked questions
Why does my Godot game stutter in the editor but not in the exported game?
The editor runs its own interface and debugger next to your game, and debug builds do extra checks. Measure an exported build before you change anything; stutter that only happens in the editor usually isn't what players see.
Should I raise the physics tick rate to match a 144 Hz screen?
It can hide jitter on that screen, but it costs physics time every frame and doesn't help other refresh rates. Physics interpolation fixes jitter for every screen at the normal tick rate.
Does the Compatibility renderer stutter more?
It doesn't get Godot 4.4's ubershaders, so first-use shader hitches matter more there, and older Android GPUs have tighter limits. Draw effects once behind the loading screen and keep shaders simple. Web and many Android exports use it.
My game uses C#. Does garbage collection cause stutter?
It can: the .NET garbage collector pauses the game when it runs. Avoid allocating every frame in _Process (new lists, LINQ, string building) and reuse objects. GDScript uses reference counting, so it doesn't have collection pauses.
Sources
- Godot docs: Physics interpolation
- Godot docs: Reducing stutter from shader (pipeline) compilations
- Godot docs: Background loading
- Godot docs: The Profiler
- Godot docs: Optimization using MultiMeshes
- Godot docs: Performance monitors
Something wrong or out of date? Email support@glitchless.gg and we'll fix it. Glitchless isn't affiliated with the Godot Foundation; product names are their owners' trademarks.