diff --git a/fsl/fsleyes/profiles/lightboxviewprofile.py b/fsl/fsleyes/profiles/lightboxviewprofile.py
index 5d032af1fd169af189e97c32aac0695e67ebbfc4..742a241ee65d38ef966f934960c2dbf09f6eff03 100644
--- a/fsl/fsleyes/profiles/lightboxviewprofile.py
+++ b/fsl/fsleyes/profiles/lightboxviewprofile.py
@@ -11,6 +11,7 @@
 import logging
 
 import fsl.fsleyes.profiles as profiles
+import fsl.utils.async      as async
 
 
 log = logging.getLogger(__name__)
@@ -76,7 +77,12 @@ class LightBoxViewProfile(profiles.Profile):
         if   wheel > 0: wheel = -1
         elif wheel < 0: wheel =  1
 
-        self._viewPanel.getCanvas().topRow += wheel
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self._viewPanel.getCanvas().topRow += wheel
+
+        async.idle(update, timeout=0.1)
 
         
     def _viewModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos):
@@ -106,4 +112,10 @@ class LightBoxViewProfile(profiles.Profile):
 
         if   wheel > 0: wheel =  50
         elif wheel < 0: wheel = -50
-        self._viewPanel.getSceneOptions().zoom += wheel 
+
+        # see comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self._viewPanel.getSceneOptions().zoom += wheel
+
+        async.idle(update, timeout=0.1)
diff --git a/fsl/fsleyes/profiles/orthoeditprofile.py b/fsl/fsleyes/profiles/orthoeditprofile.py
index 62c1947e23741653e10fc9130a616b0a8ba12ad6..2b8caa54f51ce15dfb934908fcac2d17c35bd93d 100644
--- a/fsl/fsleyes/profiles/orthoeditprofile.py
+++ b/fsl/fsleyes/profiles/orthoeditprofile.py
@@ -17,6 +17,7 @@ import numpy                        as np
 import                                 props
 import fsl.data.image               as fslimage
 import fsl.data.strings             as strings
+import fsl.utils.async              as async
 import fsl.utils.dialog             as fsldlg
 import fsl.utils.status             as status
 import fsl.fsleyes.actions          as actions
@@ -868,10 +869,17 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
 
         voxel = self.__getVoxelLocation(canvasPos)
 
-        if voxel is not None:
+        if voxel is None:
+            return
+
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
             self.__drawCursorAnnotation(canvas, voxel)
             self.__refreshCanvases(ev, canvas)
 
+        async.idle(update, timeout=0.1)
+
         
     def _deselModeLeftMouseDown(self, ev, canvas, mousePos, canvasPos):
         """Handles mouse down events in ``desel`` mode.
@@ -1056,17 +1064,24 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         dataRange = opts.dataMax - opts.dataMin
         step      = 0.01 * dataRange
 
-        if   wheel > 0: self.intensityThres += step
-        elif wheel < 0: self.intensityThres -= step
+        if   wheel > 0: offset =  step
+        elif wheel < 0: offset = -step
         else:           return
 
-        if self.__selecting:
-            
-            voxel = self.__getVoxelLocation(canvasPos) 
 
-            if voxel is not None:
-                self.__selintSelect(voxel, canvas)
-                self.__refreshCanvases(ev, canvas) 
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+            self.intensityThres += offset
+            if self.__selecting:
+
+                voxel = self.__getVoxelLocation(canvasPos) 
+
+                if voxel is not None:
+                    self.__selintSelect(voxel, canvas)
+                    self.__refreshCanvases(ev, canvas)
+
+        async.idle(update, timeout=0.1)
 
                 
     def _chradModeMouseWheel(self, ev, canvas, wheel, mousePos, canvasPos):
@@ -1077,14 +1092,22 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile):
         select-by-intensity is re-run at the current mouse location.
         """ 
 
-        if   wheel > 0: self.searchRadius -= 5
-        elif wheel < 0: self.searchRadius += 5
+        if   wheel > 0: offset = -5
+        elif wheel < 0: offset =  5
         else:           return
 
-        if self.__selecting:
-            
-            voxel = self.__getVoxelLocation(canvasPos) 
+        # See comment in OrthoViewProfile._zoomModeMouseWheel
+        # about timeout
+        def update():
+
+            self.searchRadius += offset
+
+            if self.__selecting:
+
+                voxel = self.__getVoxelLocation(canvasPos) 
+
+                if voxel is not None:
+                    self.__selintSelect(voxel, canvas)
+                    self.__refreshCanvases(ev, canvas) 
 
-            if voxel is not None:
-                self.__selintSelect(voxel, canvas)
-                self.__refreshCanvases(ev, canvas) 
+        async.idle(update, timeout=0.1)
diff --git a/fsl/fsleyes/profiles/orthoviewprofile.py b/fsl/fsleyes/profiles/orthoviewprofile.py
index 1e42294ce64f8ffaf96f50395b7f2d8dec271d7d..d4bee5dbc8e1f5cbf8923abfca22bedf3c59d0fa 100644
--- a/fsl/fsleyes/profiles/orthoviewprofile.py
+++ b/fsl/fsleyes/profiles/orthoviewprofile.py
@@ -16,6 +16,7 @@ import numpy as np
 
 import fsl.fsleyes.profiles as profiles
 import fsl.fsleyes.actions  as actions
+import fsl.utils.async      as async
 import fsl.data.image       as fslimage
 import fsl.data.constants   as constants
 
@@ -332,7 +333,11 @@ class OrthoViewProfile(profiles.Profile):
         elif ch  in ('+', '='):   dirs[canvas.zax] =  1
         elif ch  in ('-', '_'):   dirs[canvas.zax] = -1
 
-        self._displayCtx.location.xyz = self.__offsetLocation(*dirs)
+        def update():
+            self._displayCtx.location.xyz = self.__offsetLocation(*dirs)
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
         
     #####################
@@ -357,7 +362,11 @@ class OrthoViewProfile(profiles.Profile):
 
         pos = self.__offsetLocation(*dirs)
 
-        self._displayCtx.location[canvas.zax] = pos[canvas.zax]
+        def update():
+            self._displayCtx.location[canvas.zax] = pos[canvas.zax]
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
         
     ####################
@@ -378,7 +387,17 @@ class OrthoViewProfile(profiles.Profile):
         """
         if   wheel > 0: wheel =  50
         elif wheel < 0: wheel = -50
-        canvas.zoom += wheel
+
+        # Over SSH/X11, mouse wheel events seem to get queued,
+        # and continue to get processed after the user has
+        # stopped spinning the mouse wheel, which is super
+        # frustrating. So we do the update asynchronously, and
+        # set a time out to drop the event, and prevent the
+        # horribleness from happening.
+        def update():
+            canvas.zoom += wheel
+        
+        async.idle(update, timeout=0.1)
 
         
     def _zoomModeChar(self, ev, canvas, key):
@@ -500,7 +519,11 @@ class OrthoViewProfile(profiles.Profile):
         elif key == wx.WXK_RIGHT: xoff =  2
         else:                     return
 
-        canvas.panDisplayBy(xoff, yoff)
+        def update():
+            canvas.panDisplayBy(xoff, yoff)
+
+        # See comment in _zoomModeMouseWheel about timeout
+        async.idle(update, timeout=0.1)
 
 
     #############
diff --git a/fsl/utils/async.py b/fsl/utils/async.py
index 73330044b4653df87e8142df5a3178b8a367ae05..801d0debbc0516623ca095390c0ea5431182b7a3 100644
--- a/fsl/utils/async.py
+++ b/fsl/utils/async.py
@@ -31,6 +31,8 @@ task to run. It waits until all the threads have finished, and then runs
 the task (via :func:`idle`).
 """
 
+
+import time
 import Queue
 import logging
 import threading
@@ -125,12 +127,18 @@ def _wxIdleLoop(ev):
         
     ev.Skip()
 
-    try:                task, args, kwargs = _idleQueue.get_nowait()
-    except Queue.Empty: return
+    try:
+        task, schedtime, timeout, args, kwargs = _idleQueue.get_nowait()
+    except Queue.Empty:
+        return
+
+    name    = getattr(task, '__name__', '<unknown>')
+    now     = time.time()
+    elapsed = now - schedtime
 
-    name = getattr(task, '__name__', '<unknown>')
-    log.debug('Running function ({}) on wx idle loop'.format(name))
-    task(*args, **kwargs)
+    if timeout == 0 or (elapsed < timeout):
+        log.debug('Running function ({}) on wx idle loop'.format(name))
+        task(*args, **kwargs)
 
     if _idleQueue.qsize() > 0:
         ev.RequestMore()
@@ -141,6 +149,12 @@ def idle(task, *args, **kwargs):
 
     :arg task: The task to run.
 
+    :arg timeout: Optional. If provided, must be provided as a keyword
+                  argument. Specifies a time out, in seconds. If this
+                  amount of time passes before the function gets
+                  scheduled to be called on the idle loop, the function
+                  is not called, and is dropped from the queue.
+
     All other arguments are passed through to the task function.
 
     If a ``wx.App`` is not running, the task is called directly.
@@ -149,6 +163,9 @@ def idle(task, *args, **kwargs):
     global _idleRegistered
     global _idleTasks
 
+    schedtime = time.time()
+    timeout   = kwargs.pop('timeout', 0)
+
     if _haveWX():
         import wx
 
@@ -159,7 +176,7 @@ def idle(task, *args, **kwargs):
         name = getattr(task, '__name__', '<unknown>')
         log.debug('Scheduling idle task ({}) on wx idle loop'.format(name))
 
-        _idleQueue.put_nowait((task, args, kwargs))
+        _idleQueue.put_nowait((task, schedtime, timeout, args, kwargs))
             
     else:
         log.debug('Running idle task directly')