diff --git a/src/citra/config.cpp b/src/citra/config.cpp
index fe74fcd723b33c6df2f828657afbdce065c4bc1d..29462c982d375f15a8208a8abaf8b62ad6f71f1a 100644
--- a/src/citra/config.cpp
+++ b/src/citra/config.cpp
@@ -66,6 +66,8 @@ void Config::ReadValues() {
     Settings::values.use_scaled_resolution =
         sdl2_config->GetBoolean("Renderer", "use_scaled_resolution", false);
     Settings::values.use_vsync = sdl2_config->GetBoolean("Renderer", "use_vsync", false);
+    Settings::values.toggle_framelimit =
+        sdl2_config->GetBoolean("Renderer", "toggle_framelimit", true);
 
     Settings::values.bg_red = (float)sdl2_config->GetReal("Renderer", "bg_red", 1.0);
     Settings::values.bg_green = (float)sdl2_config->GetReal("Renderer", "bg_green", 1.0);
diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h
index b98dc4d83f7a235560cc1aec2979587d12d04b1f..001b18ac2e0aaf0fa29f5a66be98dd2c04e24253 100644
--- a/src/citra/default_ini.h
+++ b/src/citra/default_ini.h
@@ -64,6 +64,10 @@ use_vsync =
 # 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen
 layout_option =
 
+#Whether to toggle frame limiter on or off.
+# 0: Off , 1  (default): On
+toggle_framelimit =
+
 # Swaps the prominent screen with the other screen.
 # For example, if Single Screen is chosen, setting this to 1 will display the bottom screen instead of the top screen.
 # 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent
diff --git a/src/citra_qt/config.cpp b/src/citra_qt/config.cpp
index 3cdfe6443648398a291910f72c657b965895983e..06a4e9d25c9f5304d84344227935dab1c48b35df 100644
--- a/src/citra_qt/config.cpp
+++ b/src/citra_qt/config.cpp
@@ -47,6 +47,7 @@ void Config::ReadValues() {
     Settings::values.use_scaled_resolution =
         qt_config->value("use_scaled_resolution", false).toBool();
     Settings::values.use_vsync = qt_config->value("use_vsync", false).toBool();
+    Settings::values.toggle_framelimit = qt_config->value("toggle_framelimit", true).toBool();
 
     Settings::values.bg_red = qt_config->value("bg_red", 1.0).toFloat();
     Settings::values.bg_green = qt_config->value("bg_green", 1.0).toFloat();
@@ -152,6 +153,7 @@ void Config::SaveValues() {
     qt_config->setValue("use_shader_jit", Settings::values.use_shader_jit);
     qt_config->setValue("use_scaled_resolution", Settings::values.use_scaled_resolution);
     qt_config->setValue("use_vsync", Settings::values.use_vsync);
+    qt_config->setValue("toggle_framelimit", Settings::values.toggle_framelimit);
 
     // Cast to double because Qt's written float values are not human-readable
     qt_config->setValue("bg_red", (double)Settings::values.bg_red);
diff --git a/src/citra_qt/configure_graphics.cpp b/src/citra_qt/configure_graphics.cpp
index 29834e11baf498e0bb0c52efc0a16ce60df27a5e..36f10c8d76bc7892668b5b35eca19cf0356d0469 100644
--- a/src/citra_qt/configure_graphics.cpp
+++ b/src/citra_qt/configure_graphics.cpp
@@ -23,6 +23,7 @@ void ConfigureGraphics::setConfiguration() {
     ui->toggle_shader_jit->setChecked(Settings::values.use_shader_jit);
     ui->toggle_scaled_resolution->setChecked(Settings::values.use_scaled_resolution);
     ui->toggle_vsync->setChecked(Settings::values.use_vsync);
+    ui->toggle_framelimit->setChecked(Settings::values.toggle_framelimit);
     ui->layout_combobox->setCurrentIndex(static_cast<int>(Settings::values.layout_option));
     ui->swap_screen->setChecked(Settings::values.swap_screen);
 }
@@ -32,6 +33,7 @@ void ConfigureGraphics::applyConfiguration() {
     Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked();
     Settings::values.use_scaled_resolution = ui->toggle_scaled_resolution->isChecked();
     Settings::values.use_vsync = ui->toggle_vsync->isChecked();
+    Settings::values.toggle_framelimit = ui->toggle_framelimit->isChecked();
     Settings::values.layout_option =
         static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
     Settings::values.swap_screen = ui->swap_screen->isChecked();
diff --git a/src/citra_qt/configure_graphics.ui b/src/citra_qt/configure_graphics.ui
index af16a4292e0b6e394f298375e9e635f81f04d233..964aa0bbd00c5fc54e5ae35035fc245fd5c2f84d 100644
--- a/src/citra_qt/configure_graphics.ui
+++ b/src/citra_qt/configure_graphics.ui
@@ -50,6 +50,13 @@
           </property>
          </widget>
         </item>
+        <item>
+         <widget class="QCheckBox" name="toggle_framelimit">
+          <property name="text">
+           <string>Limit framerate</string>
+          </property>
+         </widget>
+        </item>
        </layout>
       </widget>
      </item>
diff --git a/src/core/hw/gpu.cpp b/src/core/hw/gpu.cpp
index 45dedea688b296265e697b56f9904f8b1d586e9f..cfba82e51cba2976e6b03522ef20d8b551f48100 100644
--- a/src/core/hw/gpu.cpp
+++ b/src/core/hw/gpu.cpp
@@ -8,7 +8,10 @@
 #include "common/color.h"
 #include "common/common_types.h"
 #include "common/logging/log.h"
+#include "common/math_util.h"
 #include "common/microprofile.h"
+#include "common/thread.h"
+#include "common/timer.h"
 #include "common/vector_math.h"
 #include "core/core_timing.h"
 #include "core/hle/service/gsp_gpu.h"
@@ -35,6 +38,14 @@ const u64 frame_ticks = 268123480ull / 60;
 static int vblank_event;
 /// Total number of frames drawn
 static u64 frame_count;
+/// Start clock for frame limiter
+static u32 time_point;
+/// Total delay caused by slow frames
+static float time_delay;
+constexpr float FIXED_FRAME_TIME = 1000.0f / 60;
+// Max lag caused by slow frames. Can be adjusted to compensate for too many slow frames. Higher
+// values increases time needed to limit frame rate after spikes
+constexpr float MAX_LAG_TIME = 18;
 
 template <typename T>
 inline void Read(T& var, const u32 raw_addr) {
@@ -512,6 +523,21 @@ template void Write<u32>(u32 addr, const u32 data);
 template void Write<u16>(u32 addr, const u16 data);
 template void Write<u8>(u32 addr, const u8 data);
 
+static void FrameLimiter() {
+    time_delay += FIXED_FRAME_TIME;
+    time_delay = MathUtil::Clamp(time_delay, -MAX_LAG_TIME, MAX_LAG_TIME);
+    s32 desired_time = static_cast<s32>(time_delay);
+    s32 elapsed_time = static_cast<s32>(Common::Timer::GetTimeMs() - time_point);
+
+    if (elapsed_time < desired_time) {
+        Common::SleepCurrentThread(desired_time - elapsed_time);
+    }
+
+    u32 frame_time = Common::Timer::GetTimeMs() - time_point;
+
+    time_delay -= frame_time;
+}
+
 /// Update hardware
 static void VBlankCallback(u64 userdata, int cycles_late) {
     frame_count++;
@@ -528,6 +554,12 @@ static void VBlankCallback(u64 userdata, int cycles_late) {
     // Check for user input updates
     Service::HID::Update();
 
+    if (!Settings::values.use_vsync && Settings::values.toggle_framelimit) {
+        FrameLimiter();
+    }
+
+    time_point = Common::Timer::GetTimeMs();
+
     // Reschedule recurrent event
     CoreTiming::ScheduleEvent(frame_ticks - cycles_late, vblank_event);
 }
@@ -563,6 +595,7 @@ void Init() {
     framebuffer_sub.active_fb = 0;
 
     frame_count = 0;
+    time_point = Common::Timer::GetTimeMs();
 
     vblank_event = CoreTiming::RegisterEvent("GPU::VBlankCallback", VBlankCallback);
     CoreTiming::ScheduleEvent(frame_ticks, vblank_event);
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 05f41f7987899cd7d8aeaa98f53012f1518cef1f..626e06cd924ae131f56f9d39e2dc8850ff9cfa6c 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -21,6 +21,7 @@ void Apply() {
     VideoCore::g_hw_renderer_enabled = values.use_hw_renderer;
     VideoCore::g_shader_jit_enabled = values.use_shader_jit;
     VideoCore::g_scaled_resolution_enabled = values.use_scaled_resolution;
+    VideoCore::g_toggle_framelimit_enabled = values.toggle_framelimit;
 
     if (VideoCore::g_emu_window) {
         auto layout = VideoCore::g_emu_window->GetFramebufferLayout();
diff --git a/src/core/settings.h b/src/core/settings.h
index 7470fdbeb2c4ab3725553ee65edcf9cb360e7cdf..db4c8fadaa28a94aa58d20ad6b615061d7947085 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -90,6 +90,7 @@ struct Values {
     bool use_shader_jit;
     bool use_scaled_resolution;
     bool use_vsync;
+    bool toggle_framelimit;
 
     LayoutOption layout_option;
     bool swap_screen;
diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp
index 83e33dfc2be11558fabf033da6eac7ed96b2aa5d..8db882f59a94670f2d536ce516f3b3030644ac90 100644
--- a/src/video_core/video_core.cpp
+++ b/src/video_core/video_core.cpp
@@ -21,6 +21,7 @@ std::atomic<bool> g_hw_renderer_enabled;
 std::atomic<bool> g_shader_jit_enabled;
 std::atomic<bool> g_scaled_resolution_enabled;
 std::atomic<bool> g_vsync_enabled;
+std::atomic<bool> g_toggle_framelimit_enabled;
 
 /// Initialize the video core
 bool Init(EmuWindow* emu_window) {
diff --git a/src/video_core/video_core.h b/src/video_core/video_core.h
index e2d725ab18142f5d0ae921a6ba0bc82f1a24e6b0..c397c1974f9c38779b88e308f4901c295fce186e 100644
--- a/src/video_core/video_core.h
+++ b/src/video_core/video_core.h
@@ -38,6 +38,7 @@ extern EmuWindow* g_emu_window;                  ///< Emu window
 extern std::atomic<bool> g_hw_renderer_enabled;
 extern std::atomic<bool> g_shader_jit_enabled;
 extern std::atomic<bool> g_scaled_resolution_enabled;
+extern std::atomic<bool> g_toggle_framelimit_enabled;
 
 /// Start the video core
 void Start();