diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp
index f9177d67cd..4a0c29bac3 100644
--- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp
+++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp
@@ -890,7 +890,7 @@ void Renderer::CheckForSurfaceChange()
                                                             s_new_surface_handle);
       if (surface != VK_NULL_HANDLE)
       {
-        m_swap_chain = SwapChain::Create(s_new_surface_handle, surface);
+        m_swap_chain = SwapChain::Create(s_new_surface_handle, surface, g_ActiveConfig.IsVSync());
         if (!m_swap_chain)
           PanicAlert("Failed to create swap chain.");
       }
@@ -917,7 +917,6 @@ void Renderer::CheckForSurfaceChange()
 void Renderer::CheckForConfigChanges()
 {
   // Compare g_Config to g_ActiveConfig to determine what has changed before copying.
-  bool vsync_changed = (g_Config.bVSync != g_ActiveConfig.bVSync);
   bool msaa_changed = (g_Config.iMultisamples != g_ActiveConfig.iMultisamples);
   bool ssaa_changed = (g_Config.bSSAA != g_ActiveConfig.bSSAA);
   bool anisotropy_changed = (g_Config.iMaxAnisotropy != g_ActiveConfig.iMaxAnisotropy);
@@ -963,8 +962,11 @@ void Renderer::CheckForConfigChanges()
   }
 
   // For vsync, we need to change the present mode, which means recreating the swap chain.
-  if (vsync_changed)
-    ResizeSwapChain();
+  if (m_swap_chain && g_ActiveConfig.IsVSync() != m_swap_chain->IsVSyncEnabled())
+  {
+    g_command_buffer_mgr->WaitForGPUIdle();
+    m_swap_chain->SetVSync(g_ActiveConfig.IsVSync());
+  }
 
   // Wipe sampler cache if force texture filtering or anisotropy changes.
   if (anisotropy_changed || force_texture_filtering_changed)
diff --git a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp
index 34ad5c39f2..899f931c6d 100644
--- a/Source/Core/VideoBackends/Vulkan/SwapChain.cpp
+++ b/Source/Core/VideoBackends/Vulkan/SwapChain.cpp
@@ -24,8 +24,8 @@
 
 namespace Vulkan
 {
-SwapChain::SwapChain(void* native_handle, VkSurfaceKHR surface)
-    : m_native_handle(native_handle), m_surface(surface)
+SwapChain::SwapChain(void* native_handle, VkSurfaceKHR surface, bool vsync)
+    : m_native_handle(native_handle), m_surface(surface), m_vsync_enabled(vsync)
 {
 }
 
@@ -127,9 +127,10 @@ VkSurfaceKHR SwapChain::CreateVulkanSurface(VkInstance instance, void* hwnd)
 #endif
 }
 
-std::unique_ptr<SwapChain> SwapChain::Create(void* native_handle, VkSurfaceKHR surface)
+std::unique_ptr<SwapChain> SwapChain::Create(void* native_handle, VkSurfaceKHR surface, bool vsync)
 {
-  std::unique_ptr<SwapChain> swap_chain = std::make_unique<SwapChain>(native_handle, surface);
+  std::unique_ptr<SwapChain> swap_chain =
+      std::make_unique<SwapChain>(native_handle, surface, vsync);
 
   if (!swap_chain->CreateSwapChain() || !swap_chain->CreateRenderPass() ||
       !swap_chain->SetupSwapChainImages())
@@ -198,7 +199,7 @@ bool SwapChain::SelectPresentMode()
   };
 
   // If vsync is enabled, prefer VK_PRESENT_MODE_FIFO_KHR.
-  if (g_ActiveConfig.IsVSync())
+  if (m_vsync_enabled)
   {
     // Try for relaxed vsync first, since it's likely the VI won't line up with
     // the refresh rate of the system exactly, so tearing once is better than
@@ -456,11 +457,8 @@ VkResult SwapChain::AcquireNextImage(VkSemaphore available_semaphore)
 
 bool SwapChain::ResizeSwapChain()
 {
-  if (!CreateSwapChain())
-    return false;
-
   DestroySwapChainImages();
-  if (!SetupSwapChainImages())
+  if (!CreateSwapChain() || !SetupSwapChainImages())
   {
     PanicAlert("Failed to re-configure swap chain images, this is fatal (for now)");
     return false;
@@ -469,6 +467,16 @@ bool SwapChain::ResizeSwapChain()
   return true;
 }
 
+bool SwapChain::SetVSync(bool enabled)
+{
+  if (m_vsync_enabled == enabled)
+    return true;
+
+  // Resizing recreates the swap chain with the new present mode.
+  m_vsync_enabled = enabled;
+  return ResizeSwapChain();
+}
+
 bool SwapChain::RecreateSurface(void* native_handle)
 {
   // Destroy the old swap chain, images, and surface.
diff --git a/Source/Core/VideoBackends/Vulkan/SwapChain.h b/Source/Core/VideoBackends/Vulkan/SwapChain.h
index f0ccb81921..0ff480d64c 100644
--- a/Source/Core/VideoBackends/Vulkan/SwapChain.h
+++ b/Source/Core/VideoBackends/Vulkan/SwapChain.h
@@ -19,18 +19,19 @@ class ObjectCache;
 class SwapChain
 {
 public:
-  SwapChain(void* native_handle, VkSurfaceKHR surface);
+  SwapChain(void* native_handle, VkSurfaceKHR surface, bool vsync);
   ~SwapChain();
 
   // Creates a vulkan-renderable surface for the specified window handle.
   static VkSurfaceKHR CreateVulkanSurface(VkInstance instance, void* hwnd);
 
   // Create a new swap chain from a pre-existing surface.
-  static std::unique_ptr<SwapChain> Create(void* native_handle, VkSurfaceKHR surface);
+  static std::unique_ptr<SwapChain> Create(void* native_handle, VkSurfaceKHR surface, bool vsync);
 
   void* GetNativeHandle() const { return m_native_handle; }
   VkSurfaceKHR GetSurface() const { return m_surface; }
   VkSurfaceFormatKHR GetSurfaceFormat() const { return m_surface_format; }
+  bool IsVSyncEnabled() const { return m_vsync_enabled; }
   VkSwapchainKHR GetSwapChain() const { return m_swap_chain; }
   VkRenderPass GetRenderPass() const { return m_render_pass; }
   u32 GetWidth() const { return m_width; }
@@ -54,6 +55,9 @@ public:
   bool RecreateSurface(void* native_handle);
   bool ResizeSwapChain();
 
+  // Change vsync enabled state. This may fail as it causes a swapchain recreation.
+  bool SetVSync(bool enabled);
+
 private:
   bool SelectSurfaceFormat();
   bool SelectPresentMode();
@@ -76,10 +80,11 @@ private:
     VkFramebuffer framebuffer;
   };
 
-  void* m_native_handle = nullptr;
+  void* m_native_handle;
   VkSurfaceKHR m_surface = VK_NULL_HANDLE;
   VkSurfaceFormatKHR m_surface_format = {};
   VkPresentModeKHR m_present_mode = VK_PRESENT_MODE_RANGE_SIZE_KHR;
+  bool m_vsync_enabled;
 
   VkSwapchainKHR m_swap_chain = VK_NULL_HANDLE;
   std::vector<SwapChainImage> m_swap_chain_images;
diff --git a/Source/Core/VideoBackends/Vulkan/main.cpp b/Source/Core/VideoBackends/Vulkan/main.cpp
index a9debda541..1fb8f6c6ab 100644
--- a/Source/Core/VideoBackends/Vulkan/main.cpp
+++ b/Source/Core/VideoBackends/Vulkan/main.cpp
@@ -169,7 +169,7 @@ bool VideoBackend::Initialize(void* window_handle)
   std::unique_ptr<SwapChain> swap_chain;
   if (surface != VK_NULL_HANDLE)
   {
-    swap_chain = SwapChain::Create(window_handle, surface);
+    swap_chain = SwapChain::Create(window_handle, surface, g_Config.IsVSync());
     if (!swap_chain)
     {
       PanicAlert("Failed to create Vulkan swap chain.");