diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40a27cf9fa..b2b5d68eb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -152,6 +152,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed an issue where prices weren't properly making their way from econsim to the actual trade values.
 - Fixed entities with voxel colliders being off by one physics tick for collision.
 - Airships no longer oscillate dramatically into the sky due to mistaking velocity for acceleration.
+- The login and character selection screens no longer cause high GPU usage when the framerate limit is set to Unlimited.
 
 ## [0.9.0] - 2021-03-20
 
diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs
index ae15b7a07d..4d2a1a04dd 100644
--- a/voxygen/src/lib.rs
+++ b/voxygen/src/lib.rs
@@ -132,6 +132,9 @@ pub trait PlayState {
     /// Get a descriptive name for this state type.
     fn name(&self) -> &'static str;
 
+    /// Determines whether the play state should have an enforced FPS cap
+    fn capped_fps(&self) -> bool;
+
     /// Draw the play state.
     fn render(&mut self, renderer: &mut Renderer, settings: &Settings);
 }
diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs
index 2b9acd78b6..4b8093bc8e 100644
--- a/voxygen/src/menu/char_selection/mod.rs
+++ b/voxygen/src/menu/char_selection/mod.rs
@@ -232,6 +232,8 @@ impl PlayState for CharSelectionState {
 
     fn name(&self) -> &'static str { "Character Selection" }
 
+    fn capped_fps(&self) -> bool { true }
+
     fn render(&mut self, renderer: &mut Renderer, _: &Settings) {
         let mut drawer = match renderer
             .start_recording_frame(self.scene.global_bind_group())
diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs
index 0aa482913d..ca4bd328a4 100644
--- a/voxygen/src/menu/main/mod.rs
+++ b/voxygen/src/menu/main/mod.rs
@@ -317,6 +317,8 @@ impl PlayState for MainMenuState {
 
     fn name(&self) -> &'static str { "Title" }
 
+    fn capped_fps(&self) -> bool { true }
+
     fn render(&mut self, renderer: &mut Renderer, _: &Settings) {
         let mut drawer = match renderer
             .start_recording_frame(self.scene.global_bind_group())
diff --git a/voxygen/src/run.rs b/voxygen/src/run.rs
index f1711dfc6a..35137ecad0 100644
--- a/voxygen/src/run.rs
+++ b/voxygen/src/run.rs
@@ -164,12 +164,15 @@ fn handle_main_events_cleared(
         *control_flow = winit::event_loop::ControlFlow::Exit;
     }
 
+    let mut capped_fps = false;
+
     drop(guard);
     if let Some(last) = states.last_mut() {
         span!(guard, "Render");
         let renderer = global_state.window.renderer_mut();
         // Render the screen using the global renderer
         last.render(renderer, &global_state.settings);
+        capped_fps = last.capped_fps();
 
         drop(guard);
     }
@@ -177,9 +180,21 @@ fn handle_main_events_cleared(
     if !exit {
         // Wait for the next tick.
         span!(guard, "Main thread sleep");
-        global_state.clock.set_target_dt(Duration::from_secs_f64(
-            1.0 / get_fps(global_state.settings.graphics.max_fps) as f64,
-        ));
+
+        // Enforce an FPS cap for the non-game session play states to prevent them
+        // running at hundreds/thousands of FPS resulting in high GPU usage for
+        // effectively doing nothing.
+        let max_fps = get_fps(global_state.settings.graphics.max_fps);
+        const TITLE_SCREEN_FPS_CAP: u32 = 60;
+        let target_fps = if capped_fps {
+            u32::min(TITLE_SCREEN_FPS_CAP, max_fps)
+        } else {
+            max_fps
+        };
+
+        global_state
+            .clock
+            .set_target_dt(Duration::from_secs_f64(1.0 / target_fps as f64));
         global_state.clock.tick();
         drop(guard);
         #[cfg(feature = "tracy")]
diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs
index 4cf91a6171..32c584af77 100644
--- a/voxygen/src/session/mod.rs
+++ b/voxygen/src/session/mod.rs
@@ -1387,6 +1387,8 @@ impl PlayState for SessionState {
 
     fn name(&self) -> &'static str { "Session" }
 
+    fn capped_fps(&self) -> bool { false }
+
     /// Render the session to the screen.
     ///
     /// This method should be called once per frame.