Quick answer: Application.targetFrameRate overrides vSyncCount when non-zero, and graphics drivers can force VSync off globally. Set both values explicitly at startup, document that users may need to enable VSync in their driver, and do not rely on project defaults.

Here is how to fix Unity VSync not working in a build. Your game is set to VSync Every V Blank in Quality Settings. In the editor, the frame rate caps cleanly at 60 on a 60Hz monitor. You build the game, launch it, and screen tearing is back — frames run at 400+ FPS, the GPU fan spins up, and a straight-edged object moving across the screen shows a horizontal tear line. The setting is clearly being ignored.

The Symptom

In the standalone build, frame rate is uncapped despite VSync being enabled in Quality Settings. Fast-moving content shows horizontal tearing. QualitySettings.vSyncCount reports 1 when queried at runtime, but the frame rate remains well above the display refresh. On some machines VSync works, on others it does not — indicating driver-level override. The issue is usually absent in editor play mode and only reproduces in the built player.

What Causes This

targetFrameRate override. This is the most common cause. Any script that sets Application.targetFrameRate to a non-zero value disables VSync. Unity’s documentation is clear on this: if targetFrameRate is set, vSyncCount is ignored. A common pattern is a bootstrap script setting targetFrameRate = 60 on startup, which appears to work on a 60Hz display but disables VSync and causes tearing on high-refresh-rate monitors.

Platform defaults differ. On mobile, Unity sets targetFrameRate = 30 by default. On desktop it is -1 (let VSync decide). If you set it once for mobile and forget, it persists. On WebGL, VSync is controlled by the browser compositor and the vSyncCount setting has no effect.

Driver override. Nvidia Control Panel and AMD Radeon Software both have a “Vertical sync” global setting under 3D Settings. If the user has it set to “Off” or “Use application setting” but with a Program Settings override for unity.exe, the driver ignores what your game requests. There is no API to override this from inside the game.

Compositor in windowed mode. On Windows 10/11, borderless windowed and windowed-fullscreen modes are subject to the DWM (Desktop Window Manager) compositor. DWM presents frames at the display’s refresh rate regardless of what your game submits, which looks like VSync is working. But if DWM is bypassed — true exclusive fullscreen — and VSync is off, you get tearing.

Quality level mismatch. If you have multiple quality levels defined and your build uses a different default quality level than the editor, the VSync setting on the active level is what applies. Check QualitySettings.GetQualityLevel() in the build and verify the VSync Count on that level.

The Fix

Step 1: Set both values explicitly at startup. Do not rely on Quality Settings defaults. Create a bootstrap script that runs before the first scene and sets both values together.

using UnityEngine;

public class FrameRateSetup : MonoBehaviour
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    static void Initialize()
    {
        // Let VSync control frame pacing
        QualitySettings.vSyncCount = 1;
        Application.targetFrameRate = -1;
    }
}

The RuntimeInitializeOnLoadMethod attribute ensures this runs before any scene loads, so no other script can set targetFrameRate first and have it stick. If you explicitly want a frame cap (such as 60 FPS on a 144Hz display), disable VSync and use targetFrameRate instead.

Step 2: Audit every targetFrameRate assignment. Search your entire project (including packages and imported assets) for targetFrameRate. Third-party plugins, mobile porting kits, and sample projects often set it without warning. If any other script sets it to a non-zero value after your bootstrap runs, VSync will stop working.

// Search pattern for audit
grep -rn "targetFrameRate" Assets/ Packages/

Step 3: Verify at runtime. Add a debug display or log the values after scene load to confirm what is actually active. You might be surprised to find a package you installed overrode your settings.

void Start()
{
    Debug.Log($"vSyncCount: {QualitySettings.vSyncCount}, " +
        $"targetFrameRate: {Application.targetFrameRate}, " +
        $"display refresh: {Screen.currentResolution.refreshRateRatio.value}");
}

Step 4: Use exclusive fullscreen for reliable VSync. In Player Settings, under Resolution and Presentation, set “Fullscreen Mode” to “Exclusive Fullscreen”. Borderless Window and Windowed modes both use the DWM compositor on Windows, which has its own frame pacing. Exclusive Fullscreen bypasses it and gives you direct control over VSync behavior.

Understanding vSyncCount Values

vSyncCount = 1 syncs to every VBlank (60 FPS on a 60Hz display). vSyncCount = 2 syncs every other VBlank (30 FPS on 60Hz). vSyncCount = 0 disables VSync entirely, letting targetFrameRate control frame pacing. Values above 2 are technically allowed but rarely useful.

On variable refresh rate displays (G-Sync, FreeSync), leave vSyncCount = 0 and set targetFrameRate to slightly below the maximum refresh rate. This gives G-Sync the range it needs to work properly. Setting vSyncCount = 1 with VRR can cause the display to hunt between refresh rates.

Giving Users Control

Do not lock VSync on or off. Expose a settings menu option that writes to QualitySettings.vSyncCount. Some users have gaming monitors with VRR, some have traditional 60Hz panels, and some are sensitive to input latency from VSync. Let them choose. Persist the choice to PlayerPrefs or a settings file and restore on startup.

“Every bootstrap script that sets targetFrameRate is a bug waiting to happen. Set it once, at startup, and never again.”

Related Issues

If you see stuttering despite correct VSync, check Unity Physics Jittery Movement for interpolation fixes. For frame rate drops not related to VSync, Debugging Frame Rate Drops covers profiling patterns.

Set vSyncCount and targetFrameRate together in one RuntimeInitializeOnLoadMethod. Never again.