Stabilizing FPS in Godot: Frame Rate Management and Optimization

Created: 2025-12-10Last updated: 2026-07-09

How to eliminate stutter in Godot games. Illustrates the difference between physics ticks and rendering frames, V-Sync and max_fps, physics interpolation that kills jitter for good, and a measurement-first approach to optimization.

"I turned on V-Sync but it still isn't smooth." "It only stutters the moment I spawn a lot of enemies." "I'm never sure whether to use _process or _physics_process." Frame rate problems clear up remarkably fast once you understand that in Godot, rendering and physics run on separate cycles.

This article starts with the root cause of stutter (jitter), then covers frame rate control via V-Sync and max_fps, physics interpolation that kills jitter for good, and the "measure before you optimize" mindset, all with diagrams and code.

Frame rate management illustration contrasting choppy, stair-stepped motion (jitter) with smoothly interpolated motion, showing how stable FPS underpins the game experience

What You'll Learn

  • What stutter really is: the mismatch between physics ticks and rendering frames
  • Frame rate control with V-Sync and Engine.max_fps
  • Killing jitter with physics interpolation (one setting, plus manual interpolation)
  • "Don't guess, measure": the order of optimization based on the profiler

Sponsored

Why It Stutters: Physics Ticks vs. Rendering Frames

The key to understanding Godot's performance is separating two independent cycles: the physics tick and the rendering frame. The fact that these two run at different speeds is the direct cause of jitter.

Diagram of the mismatch between physics ticks and rendering frames: the top row shows evenly spaced physics ticks at a fixed 60Hz, the bottom row shows finer variable rendering frames at 144Hz, with multiple draws squeezed between physics updates so the object sits still and then jumps at the next tick in a stair-stepped motion
Physics tickRendering frame
RolePhysics and collision (_physics_process)Drawing and input (_process)
RateFixed (60Hz by default)Variable (depends on performance and V-Sync)
deltaFixed value (1 / physics rate)Variable value (time since the last frame)

Suppose physics runs at 60Hz and your monitor at 144Hz. The physics engine only updates positions once every 1/60 second, while the screen tries to draw every 1/144 second, so an object stays at the same position across several drawn frames and then suddenly jumps at the next physics tick. That stair-stepped motion is what jitter is. This is why "input and drawing in _process, physical movement in _physics_process" is the basic rule.

Frame Rate Control: V-Sync and max_fps

First, let's control the frame rate on the rendering side. There are two tools: V-Sync (vertical synchronization) and Engine.max_fps.

Choosing a V-Sync Mode

V-Sync synchronizes drawing with the monitor's refresh rate, preventing tearing where the screen switches mid-draw. Configure it under Display > Window > Vsync > Vsync Mode.

Comparison of the four V-Sync modes, arranged by tearing and input latency: Disabled (no sync, minimal latency but heavy tearing), Enabled (prevents tearing but adds input latency when FPS drops), Adaptive (syncs only at high FPS), and Mailbox (limits both tearing and latency)
ModeOverviewGood for
DisabledNo sync. No FPS capMinimizing input latency (tearing will appear)
EnabledAlways syncedCompletely preventing tearing (more latency when FPS drops)
AdaptiveSyncs only at high FPSPreventing tearing while avoiding latency on FPS drops
MailboxKeeps and displays the newest frameLimiting both tearing and latency (uses more VRAM)

Warning: If your Godot setting seems to have no effect, NVIDIA/AMD driver settings may be overriding it. Set the driver to "controlled by application."

Locking with Engine.max_fps

When you want to lock to a specific FPS (a retro look, power saving on mobile, and so on), use Engine.max_fps.

func _ready() -> void:
    Engine.max_fps = 60   # Lock the max FPS to 60 (effective when V-Sync is Disabled)

Set it once and it applies to the whole game. Just remember that max_fps is ignored while V-Sync is Enabled.

Sponsored

Killing Jitter with Physics Interpolation

Even with the frame rate under control, the mismatch itself between physics and rendering remains. Physics interpolation is what fills that gap. It smoothly interpolates positions between fixed physics ticks at draw time, erasing the stair-stepped motion. It's the single most effective fix for jitter.

Before-and-after diagram of physics interpolation: without it, the object only appears at physics tick positions and jumps choppily in between (stair-stepped); with it, positions between physics ticks are computed and drawn as smooth, straight motion

Enabling It in Project Settings (Recommended)

Since Godot 4.3, physics interpolation is supported out of the box in both 2D and 3D. In most cases, this setting alone solves the problem.

  1. Open Project Settings
  2. Set Physics > Common > Physics Interpolation to On

Nodes affected by physics, such as CharacterBody2D/3D and RigidBody2D/3D, are now interpolated automatically and move smoothly.

Warning: With physics interpolation enabled, node positions are drawn at their interpolated visual position. Touching global_position directly between physics ticks conflicts with interpolation, so do your movement inside _physics_process.

Hands-On: Building Stutter-Free Camera Follow

Physics interpolation applies automatically to physics nodes, but when you make a non-physics node like a camera or UI follow a physics node, you need manual interpolation. Following the hero in an action game, an in-car camera in a racer, the viewpoint in a top-down ARPG: all of them want a camera that tracks the player smoothly.

The key is Engine.get_physics_interpolation_fraction(). It returns where the current rendering frame sits between physics ticks, from 0.0 to 1.0. Use it to compute the target's true position at draw time.

Diagram of two-stage camera follow interpolation: stage one interpolates the target's position between physics ticks with get_physics_interpolation_fraction to find its true current position, stage two eases the camera itself toward that position with smoothing
# smooth_camera_2d.gd (attach to a Camera2D)
extends Camera2D

@export var target: Node2D
@export var smoothing: float = 0.1     # Smaller means a lazier follow

var _previous_target_position: Vector2

func _ready() -> void:
    if target:
        global_position = target.global_position
        _previous_target_position = target.global_position

func _process(_delta: float) -> void:
    if not target:
        return
    # Stage 1: interpolate between physics ticks to find the target's true current position
    var fraction := Engine.get_physics_interpolation_fraction()
    var interpolated := _previous_target_position.lerp(target.global_position, fraction)
    # Stage 2: ease the camera itself toward that position
    global_position = global_position.lerp(interpolated, smoothing)

func _physics_process(_delta: float) -> void:
    if target:
        # Record the position at this physics tick for next frame's interpolation
        _previous_target_position = target.global_position

There are two key points.

  • Two lerps create the smoothness: stage one interpolates between physics ticks to find the target's true current position, and stage two eases the camera toward it. With only stage one you keep the physics-rate choppiness; with only stage two you keep the target's abrupt movements.
  • Draw in _process, record in _physics_process: the interpolation math runs every frame (_process), while recording the previous position runs on the physics tick (_physics_process). Splitting those roles is the trick.

For camera setup in general, Practical Camera2D Techniques is also worth a look.

Sponsored

Measure Before You Optimize

When FPS drops with lots of objects on screen, it's tempting to jump straight to "there are too many bullets, time for object pooling." But the iron rule of optimization is "don't guess, measure."

Diagram of the measure-then-optimize flow: identify the bottleneck with Godot's Profiler and Monitors first, then pick a fix that matches the cause, such as algorithm improvements, physics interpolation, or object pooling, instead of jumping to optimization on a hunch

Godot ships with Monitors in the debugger (FPS and memory over time) and a Profiler (per-function timings). Use them to pinpoint what's actually slow first, then pick a fix that matches the cause. If the bottleneck is node creation and destruction, look at object pooling. If it's rendering cost, revisit your shaders and draw counts. If it's load times, see dynamic loading and resource management. The measurements tell you which move to make.

Object pooling in particular should only be introduced once measurement shows that short-lived, frequently spawned objects like bullets and effects are the bottleneck. Applying it to low-frequency things like bosses or UI just complicates the code with no benefit (for the full implementation, see the Complete Guide to Object Pooling).

Common Mistakes and Best Practices

Common mistakeBest practice
Putting movement and all logic in _processPhysical movement in _physics_process, drawing and input in _process. Understand what delta means in each
Introducing object pooling on a hunchPinpoint the bottleneck with the profiler, then optimize
Calling queue_free() directly inside _physics_processUse call_deferred("queue_free") to delete safely after the physics step completes
Raising the physics tick rate to fight jitterKeep the tick rate fixed at 60Hz and leave jitter to physics interpolation

Deleting during a physics step is an especially common source of accidents. Removing a node in the middle of collision handling can raise errors, so queueing queue_free() via call_deferred() is the safe move.

Bonus: Good to Know for Later

  • Always multiply by delta: _process is called a different number of times depending on the frame rate. If you don't multiply movement by delta, speed changes on PCs with different FPS. Separately from physics interpolation, treat this as a fundamental.
  • Balance power use on mobile: high FPS drains battery. On mobile it's common to keep Engine.max_fps modest (30 to 60).
  • Profile the scene that feels heavy: you don't need to profile constantly. Measuring while reproducing the stuttering scene finds the cause fastest.

Summary

  • Stutter (jitter) is the mismatch between fixed physics ticks and variable rendering frames
  • V-Sync fights tearing, Engine.max_fps locks FPS. max_fps is ignored while V-Sync is Enabled
  • The best fix for jitter is physics interpolation (Physics Interpolation set to On)
  • For non-physics nodes like cameras, follow with manual interpolation using get_physics_interpolation_fraction()
  • Optimize only after measuring. Use object pooling only when measurement shows short-lived, high-frequency objects are the bottleneck

Start by turning Physics Interpolation On in Project Settings and feeling the motion smooth out. That one change resolves most stutter.

Further Reading