diff --git a/ATxCommon/ATxCommon.csproj b/ATxCommon/ATxCommon.csproj
index 96b7ac83d3d63bb003ea2210b47566ca1f5342fd..d281301c0b8684077a38c1f8a74b267414ed8809 100644
--- a/ATxCommon/ATxCommon.csproj
+++ b/ATxCommon/ATxCommon.csproj
@@ -47,6 +47,7 @@
     <Compile Include="BuildDetails.cs" />
     <Compile Include="Conv.cs" />
     <Compile Include="FsUtils.cs" />
+    <Compile Include="Monitoring\Cpu.cs" />
     <Compile Include="NLog\RateLimitWrapper.cs" />
     <Compile Include="Serializables\DriveToCheck.cs" />
     <Compile Include="Serializables\ServiceConfig.cs" />
diff --git a/ATxCommon/Monitoring/Cpu.cs b/ATxCommon/Monitoring/Cpu.cs
new file mode 100644
index 0000000000000000000000000000000000000000..71aeb8529149404e0a0f3be008883fac789996a9
--- /dev/null
+++ b/ATxCommon/Monitoring/Cpu.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Timers;
+using NLog;
+using Timer = System.Timers.Timer;
+
+namespace ATxCommon.Monitoring
+{
+    /// <summary>
+    /// CPU load monitoring class, constantly checking the current load at the given <see
+    /// cref="Interval"/> using a separate timer (thus running in its own thread).
+    /// 
+    /// The load is determined using a <see cref="PerformanceCounter"/>, and is compared against
+    /// a configurable <see cref="Limit"/>. If the load changes from below the limit to above, a
+    /// <see cref="LoadAboveLimit"/> event will be raised. If the load has been above the limit
+    /// and is then dropping below, an <see cref="OnLoadBelowLimit"/> event will be raised as soon
+    /// as a given number of consecutive measurements (defined via <see cref="Probation"/>) were
+    /// found to be below the limit.
+    /// </summary>
+    public class Cpu
+    {
+        private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+        /// <summary>
+        /// The generic event handler delegate for CPU events.
+        /// </summary>
+        public delegate void EventHandler(object sender, EventArgs e);
+        
+        /// <summary>
+        /// Event raised when the CPU load exceeds the configured limit for any measurement.
+        /// </summary>
+        public event EventHandler LoadAboveLimit;
+
+        /// <summary>
+        /// Event raised when the CPU load is below the configured limit for at least four
+        /// consecutive measurements after having exceeded this limit before.
+        /// </summary>
+        public event EventHandler LoadBelowLimit;
+
+        private readonly Timer _monitoringTimer;
+        private readonly PerformanceCounter _cpuCounter;
+        private readonly float[] _loadReadings = {0F, 0F, 0F, 0F};
+
+        private int _interval;
+        private int _limit;
+        private int _behaving;
+        private int _probation;
+
+
+        /// <summary>
+        /// Current CPU load (usage percentage over all cores), averaged of the last four readings.
+        /// </summary>
+        /// <returns>The average CPU load from the last four readings.</returns>
+        public float Load { get; private set; }
+
+        /// <summary>
+        /// Flag representing whether the load is considered to be high or low.
+        /// </summary>
+        public bool HighLoad { get; private set; }
+
+        /// <summary>
+        /// How often (in ms) to check the CPU load.
+        /// </summary>
+        public int Interval {
+            get => _interval;
+            set {
+                _interval = value;
+                _monitoringTimer.Interval = value;
+                Log.Debug("CPU monitoring interval: {0}", _interval);
+            }
+        }
+
+        /// <summary>
+        /// Upper limit of CPU load (usage in % over all cores) before it is classified as "high".
+        /// </summary>
+        public int Limit {
+            get => _limit;
+            set {
+                _limit = value;
+                Log.Debug("CPU monitoring limit: {0}", _limit);
+            }
+        }
+
+        /// <summary>
+        /// Number of cycles where the CPU load value has to be below the limit before it is
+        /// classified as "low" again.
+        /// </summary>
+        public int Probation {
+            get => _probation;
+            set {
+                _probation = value;
+                Log.Debug("CPU monitoring probation cycles when violating limit: {0}", _probation);
+            }
+        }
+        
+        /// <summary>
+        /// Indicating whether the CPU load monitoring is active.
+        /// </summary>
+        public bool Enabled {
+            get => _monitoringTimer.Enabled;
+            set {
+                Log.Debug("{0} CPU monitoring.", value ? "Enabling" : "Disabling");
+                _monitoringTimer.Enabled = value;
+            }
+        }
+
+
+
+        /// <summary>
+        /// Create performance counter and initialize it.
+        /// </summary>
+        public Cpu() {
+            _interval = 250;
+            _limit = 25;
+            _probation = 40;
+            Log.Debug("Initializing CPU monitoring...");
+            try {
+                _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
+                Log.Debug("CPU monitoring initializing PerformanceCounter (takes one second)...");
+                _cpuCounter.NextValue();
+                Thread.Sleep(1000);
+                var curLoad = _cpuCounter.NextValue();
+                Log.Debug("CPU monitoring current load: {0:0.0}", curLoad);
+                // now initialize the load state:
+                HighLoad = curLoad > _limit;
+                _monitoringTimer = new Timer(_interval);
+                _monitoringTimer.Elapsed += UpdateCpuLoad;
+            }
+            catch (Exception) {
+                Log.Error("Initializing CPU monitoring failed!");
+                throw;
+            }
+
+            Log.Debug("Initializing CPU monitoring completed.");
+        }
+
+        /// <summary>
+        /// Check current CPU load, update the history of readings and trigger the corresponding
+        /// events if the required criteria are met.
+        /// </summary>
+        private void UpdateCpuLoad(object sender, ElapsedEventArgs e) {
+            _monitoringTimer.Enabled = false;
+            try {
+                // ConstrainedCopy seems to be the most efficient approach to shift the array:
+                Array.ConstrainedCopy(_loadReadings, 1, _loadReadings, 0, 3);
+                _loadReadings[3] = _cpuCounter.NextValue();
+                Load = _loadReadings.Average();
+                if (_loadReadings[3] > _limit) {
+                    if (_behaving > _probation) {
+                        // this means the load was considered as "low" before, so raise an event:
+                        OnLoadAboveLimit();
+                        Log.Trace("CPU load ({0:0.0}) violating limit ({1})!", _loadReadings[3], _limit);
+                    } else if (_behaving > 0) {
+                        // this means we were still in probation, so no need to trigger again...
+                        Log.Trace("Resetting behaving counter to 0 (was {0}).", _behaving);
+                    }
+                  _behaving = 0;
+                } else {
+                    _behaving++;
+                    if (_behaving == _probation) {
+                        Log.Trace("CPU load below limit for {0} cycles, passing probation!", _probation);
+                        OnLoadBelowLimit();
+                    } else if (_behaving > _probation) {
+                        Log.Trace("CPU load behaving well since {0} cycles.", _behaving);
+                    } else if (_behaving < 0) {
+                        Log.Warn("Integer wrap around happened, resetting probation counter!");
+                        _behaving = _probation + 1;
+                    }
+                }
+            }
+            catch (Exception ex) {
+                Log.Error("UpdateCpuLoad failed: {0}", ex.Message);
+            }
+            finally {
+                _monitoringTimer.Enabled = true;
+            }
+            Log.Trace("CPU load: {0:0.0} {1}", _loadReadings[3],
+                _loadReadings[3] < Limit ? " [" + _behaving + "]" : "");
+        }
+
+        /// <summary>
+        /// Raise the "LoadAboveLimit" event.
+        /// </summary>
+        protected virtual void OnLoadAboveLimit() {
+            HighLoad = true;
+            LoadAboveLimit?.Invoke(this, EventArgs.Empty);
+        }
+
+        /// <summary>
+        /// Raise the "LoadBelowLimit" event.
+        /// </summary>
+        protected virtual void OnLoadBelowLimit() {
+            HighLoad = false;
+            LoadBelowLimit?.Invoke(this, EventArgs.Empty);
+        }
+    }
+}
diff --git a/ATxCommon/Serializables/ServiceStatus.cs b/ATxCommon/Serializables/ServiceStatus.cs
index a7082c0aa9345ac4adc92c254b162478b43767c2..5df0e42f97ef1261d58aa3a6c1f953cb8eff457b 100644
--- a/ATxCommon/Serializables/ServiceStatus.cs
+++ b/ATxCommon/Serializables/ServiceStatus.cs
@@ -20,7 +20,7 @@ namespace ATxCommon.Serializables
         private DateTime _lastAdminNotification;
         private DateTime _lastGraceNotification;
 
-        private string _limitReason;
+        private string _statusDescription;
         private string _currentTransferSrc;
         private string _currentTargetTmp;
 
@@ -151,13 +151,13 @@ namespace ATxCommon.Serializables
         /// <summary>
         /// String indicating why the service is currently suspended (empty if not suspended).
         /// </summary>
-        public string LimitReason {
-            get => _limitReason;
+        public string StatusDescription {
+            get => _statusDescription;
             set {
-                if (_limitReason == value) return;
+                if (_statusDescription == value) return;
 
-                _limitReason = value;
-                Log.Trace("LimitReason was updated ({0}).", value);
+                _statusDescription = value;
+                Log.Trace("StatusDescription was updated ({0}).", value);
                 Serialize();
             }
         }
@@ -286,6 +286,27 @@ namespace ATxCommon.Serializables
                 Environment.MachineName,
                 _currentTargetTmp);
         }
+
+        /// <summary>
+        /// Helper to set the service state, logging a message if the state has changed.
+        /// </summary>
+        /// <param name="suspended">The target state for <see cref="ServiceSuspended"/>.</param>
+        /// <param name="description">The description to use for the log message as well as for
+        /// setting <see cref="StatusDescription"/>.</param>
+        public void SetSuspended(bool suspended, string description) {
+            if (description == _statusDescription && suspended == _serviceSuspended)
+                return;
+
+            _serviceSuspended = suspended;
+            _statusDescription = description;
+            Serialize();
+
+            if (suspended) {
+                Log.Info("Service suspended. Reason(s): [{0}]", description);
+            } else {
+                Log.Info("Service resuming operation ({0}).", description);
+            }
+        }
         
 
         #region validate and report
diff --git a/ATxCommon/SystemChecks.cs b/ATxCommon/SystemChecks.cs
index 0fc8b2fe9e955481f41ff210160a10a895ed7aad..eb412ce481ba100a8f6d49e1beec6364ec25a475 100644
--- a/ATxCommon/SystemChecks.cs
+++ b/ATxCommon/SystemChecks.cs
@@ -15,7 +15,7 @@ namespace ATxCommon
         /// <summary>
         /// Get the available physical memory in MB.
         /// </summary>
-        /// <returns>The available physical memory in MB or -1 in case of an error.</returns>
+        /// <returns>Available physical memory in MB or -1 in case of an error.</returns>
         public static long GetFreeMemory() {
             try {
                 var searcher =
@@ -26,30 +26,7 @@ namespace ATxCommon
                 }
             }
             catch (Exception ex) {
-                Log.Warn("Error in GetFreeMemory: {0}", ex.Message);
-            }
-
-            return -1;
-        }
-
-        /// <summary>
-        /// Get the CPU usage in percent over all cores.
-        /// </summary>
-        /// <returns>CPU usage in percent or -1 if an error occured.</returns>
-        public static int GetCpuUsage() {
-            // TODO: fix bug #36
-            try {
-                var searcher = new ManagementObjectSearcher("select * from Win32_PerfFormattedData_PerfOS_Processor");
-                foreach (var mo in searcher.Get()) {
-                    var obj = (ManagementObject)mo;
-                    var usage = obj["PercentProcessorTime"];
-                    var name = obj["Name"];
-                    if (name.ToString().Equals("_Total"))
-                        return Convert.ToInt32(usage);
-                }
-            }
-            catch (Exception ex) {
-                Log.Warn("Error in GetCpuUsage: {0}", ex.Message);
+                Log.Trace("Error in GetFreeMemory: {0}", ex.Message);
             }
 
             return -1;
diff --git a/ATxDiagnostics/ATxDiagnostics.cs b/ATxDiagnostics/ATxDiagnostics.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f3b80fb81e11a53514f5fe5ee46bd04fcc0914d3
--- /dev/null
+++ b/ATxDiagnostics/ATxDiagnostics.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Diagnostics;
+using System.Management;
+using System.Reflection;
+using ATxCommon;
+using ATxCommon.Monitoring;
+using NLog;
+using NLog.Config;
+using NLog.Targets;
+
+namespace ATxDiagnostics
+{
+    class ATxDiagnostics
+    {
+        private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+        static void Main(string[] args) {
+            var loglevel = LogLevel.Debug;
+            if (args.Length > 0 && args[0] == "trace") {
+                loglevel = LogLevel.Trace;
+            }
+            var logConfig = new LoggingConfiguration();
+            var logTargetConsole = new ConsoleTarget {
+                Name = "console",
+                Header = "AutoTx Diagnostics",
+                Layout = @"${time} [${level}] ${message}",
+            };
+            logConfig.AddTarget(logTargetConsole);
+            var logRuleConsole = new LoggingRule("*", loglevel, logTargetConsole);
+            logConfig.LoggingRules.Add(logRuleConsole);
+            LogManager.Configuration = logConfig;
+
+            var commonAssembly = Assembly.GetAssembly(typeof(ATxCommon.Monitoring.Cpu));
+            var commonVersionInfo = FileVersionInfo.GetVersionInfo(commonAssembly.Location);
+            Log.Info("ATxCommon library version: {0}", commonVersionInfo.ProductVersion);
+
+            Log.Debug("Free space on drive [C:]: " + Conv.BytesToString(SystemChecks.GetFreeDriveSpace("C:")));
+            
+            Log.Info("Checking CPU load using ATxCommon.Monitoring...");
+            var cpu = new Cpu {
+                Interval = 250,
+                Limit = 25,
+                Probation = 4,  // 4 * 250 ms = 1 second
+                Enabled = true
+            };
+            System.Threading.Thread.Sleep(10000);
+            cpu.Enabled = false;
+            Log.Info("Finished checking CPU load using ATxCommon.Monitoring.\n");
+
+            Log.Info("Checking CPU load using WMI...");
+            for (int i = 0; i < 10; i++) {
+                WmiQueryCpuLoad();
+                System.Threading.Thread.Sleep(1000);
+            }
+            Log.Info("Finished checking CPU load using WMI.\n");
+        }
+
+        private static int WmiQueryCpuLoad() {
+            Log.Trace("Querying WMI for CPU load...");
+            var watch = Stopwatch.StartNew();
+            var queryString = "SELECT Name, PercentProcessorTime " +
+                              "FROM Win32_PerfFormattedData_PerfOS_Processor";
+            var opts = new EnumerationOptions {
+                Timeout = new TimeSpan(0, 0, 10),
+                ReturnImmediately = false,
+            };
+            var searcher = new ManagementObjectSearcher("", queryString, opts);
+            Int32 usageInt32 = -1;
+
+            var managementObjects = searcher.Get();
+            if (managementObjects.Count > 0) {
+                Log.Trace("WMI query returned {0} objects.", managementObjects.Count);
+                foreach (var mo in managementObjects) {
+                    var obj = (ManagementObject)mo;
+                    var usage = obj["PercentProcessorTime"];
+                    var name = obj["Name"];
+
+                    usageInt32 = Convert.ToInt32(usage);
+                    Log.Debug("CPU usage {1}: {0}", usageInt32, name);
+
+                }
+            } else {
+                Log.Error("No objects returned from WMI!");
+            }
+
+            managementObjects.Dispose();
+            searcher.Dispose();
+
+            watch.Stop();
+            Log.Trace("WMI query took {0} ms.", watch.ElapsedMilliseconds);
+
+            return usageInt32;
+        }
+
+    }
+}
diff --git a/ATxDiagnostics/ATxDiagnostics.csproj b/ATxDiagnostics/ATxDiagnostics.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..140cd0fd36f439cc19e9b0d5d4a2c510323d2893
--- /dev/null
+++ b/ATxDiagnostics/ATxDiagnostics.csproj
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <RootNamespace>ATxDiagnostics</RootNamespace>
+    <AssemblyName>AutoTxDiagnostics</AssemblyName>
+    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <TargetFrameworkProfile />
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>
+    </DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
+      <HintPath>..\packages\NLog.4.3.11\lib\net45\NLog.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+  </ItemGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Management" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="ATxDiagnostics.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\ATxCommon\ATxCommon.csproj">
+      <Project>{166D65D5-EE10-4364-8AA3-4D86BA5CE244}</Project>
+      <Name>ATxCommon</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <PropertyGroup>
+    <PreBuildEvent>PowerShell -NoProfile -ExecutionPolicy RemoteSigned $(SolutionDir)Scripts\Prepare-Build.ps1 -SolutionDir $(SolutionDir) -ConfigurationName $(ConfigurationName)</PreBuildEvent>
+  </PropertyGroup>
+</Project>
\ No newline at end of file
diff --git a/ATxDiagnostics/App.config b/ATxDiagnostics/App.config
new file mode 100644
index 0000000000000000000000000000000000000000..d1428ad712d72a50520dab5091ddcb5ea2b5f3e6
--- /dev/null
+++ b/ATxDiagnostics/App.config
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
+    </startup>
+</configuration>
diff --git a/ATxDiagnostics/Properties/AssemblyInfo.cs b/ATxDiagnostics/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000000000000000000000000000000000..31539f5897346cc8b639a8421e160b9ef839da4c
--- /dev/null
+++ b/ATxDiagnostics/Properties/AssemblyInfo.cs
@@ -0,0 +1,44 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using ATxCommon;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("AutoTx Diagnostics")]
+[assembly: AssemblyDescription("AutoTx Command Line Diagnostics Tool")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("IMCF, Biozentrum, University of Basel")]
+[assembly: AssemblyProduct("AutoTx")]
+[assembly: AssemblyCopyright("© University of Basel 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("1fda9634-87c9-4c25-ad12-bf79da61d44d")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion(BuildDetails.GitMajor + "." +
+                           BuildDetails.GitMinor + "." +
+                           BuildDetails.GitPatch + ".0")]
+[assembly: AssemblyFileVersion(BuildDetails.GitMajor + "." +
+                               BuildDetails.GitMinor + "." +
+                               BuildDetails.GitPatch + ".0")]
+
+[assembly: AssemblyInformationalVersion(BuildDetails.BuildDate +
+                                        " " + BuildDetails.GitCommit +
+                                        " (" + BuildDetails.GitBranch + ")")]
\ No newline at end of file
diff --git a/ATxService/AutoTx.cs b/ATxService/AutoTx.cs
index 18cb06aff7ed2d5f94d9a05a6205cd98c0fb594d..58cbcc9833e25effec1415d5ecc1cf27ff4018dd 100644
--- a/ATxService/AutoTx.cs
+++ b/ATxService/AutoTx.cs
@@ -8,6 +8,7 @@ using System.IO;
 using System.Reflection;
 using System.Timers;
 using ATxCommon;
+using ATxCommon.Monitoring;
 using ATxCommon.NLog;
 using ATxCommon.Serializables;
 using NLog;
@@ -39,9 +40,26 @@ namespace ATxService
         /// </summary>
         private readonly List<string> _incomingIgnore = new List<string>();
 
+        /// <summary>
+        /// The CPU load monitoring object.
+        /// </summary>
+        private readonly Cpu _cpu;
+
         private RoboCommand _roboCommand;
+        
+        /// <summary>
+        /// Size of the file currently being transferred (in bytes). Zero if no transfer running.
+        /// </summary>
         private long _txCurFileSize;
+
+        /// <summary>
+        /// Progress (in percent) of the file currently being transferred. Zero if no transfer.
+        /// </summary>
         private int _txCurFileProgress;
+
+        /// <summary>
+        /// Internal counter to introduce a delay between two subsequent transfers.
+        /// </summary>
         private int _waitCyclesBeforeNextTx;
 
         private DateTime _lastUserDirCheck = DateTime.MinValue;
@@ -89,6 +107,28 @@ namespace ATxService
             LoadSettings();
             CreateIncomingDirectories();
 
+            try {
+                _cpu = new Cpu {
+                    Interval = 250,
+                    Limit = _config.MaxCpuUsage,
+                    Probation = 16,
+                    Enabled = true
+                };
+                _cpu.LoadAboveLimit += OnLoadAboveLimit;
+                _cpu.LoadBelowLimit += OnLoadBelowLimit;
+            }
+            catch (UnauthorizedAccessException ex) {
+                Log.Error("Not enough permissions to monitor the CPU load.\nMake sure the " +
+                          "service account is a member of the [Performance Monitor Users] " +
+                          "system group.\nError message was: {0}", ex.Message);
+                throw;
+            }
+            catch (Exception ex) {
+                Log.Error("Unexpected error initializing CPU monitoring: {0}", ex.Message);
+                throw;
+            }
+
+
             if (_config.DebugRoboSharp) {
                 Debugger.Instance.DebugMessageEvent += HandleDebugMessage;
                 Log.Debug("Enabled RoboSharp debug logging.");
@@ -496,52 +536,56 @@ namespace ATxService
 
         #region general methods
 
+        /// <summary>
+        /// Event handler for CPU load dropping below the configured limit.
+        /// </summary>
+        private void OnLoadBelowLimit(object sender, EventArgs e) {
+            Log.Trace("Received a low-CPU-load event!");
+            ResumePausedTransfer();
+        }
+
+        /// <summary>
+        /// Event handler for CPU load exceeding the configured limit.
+        /// </summary>
+        private void OnLoadAboveLimit(object sender, EventArgs e) {
+            Log.Trace("Received a high-CPU-load event!");
+            PauseTransfer();
+        }
+
         /// <summary>
         /// Check system parameters for valid ranges and update the global service state accordingly.
         /// </summary>
         private void UpdateServiceState() {
-            var limitReason = "";
+            var suspendReasons = new List<string>();
 
             // check all system parameters for valid ranges and remember the reason in a string
             // if one of them is failing (to report in the log why we're suspended)
-            if (SystemChecks.GetCpuUsage() >= _config.MaxCpuUsage)
-                limitReason = "CPU usage";
-            else if (SystemChecks.GetFreeMemory() < _config.MinAvailableMemory)
-                limitReason = "RAM usage";
-            else {
-                var blacklistedProcess = SystemChecks.CheckForBlacklistedProcesses(
-                    _config.BlacklistedProcesses);
-                if (blacklistedProcess != "") {
-                    limitReason = "blacklisted process '" + blacklistedProcess + "'";
-                }
+            if (_cpu.HighLoad)
+                suspendReasons.Add("CPU");
+
+            if (SystemChecks.GetFreeMemory() < _config.MinAvailableMemory)
+                suspendReasons.Add("RAM");
+
+            var blacklistedProcess = SystemChecks.CheckForBlacklistedProcesses(
+                _config.BlacklistedProcesses);
+            if (!string.IsNullOrWhiteSpace(blacklistedProcess)) {
+                suspendReasons.Add("process '" + blacklistedProcess + "'");
             }
             
             // all parameters within valid ranges, so set the state to "Running":
-            if (string.IsNullOrEmpty(limitReason)) {
-                _status.ServiceSuspended = false;
-                if (!string.IsNullOrEmpty(_status.LimitReason)) {
-                    _status.LimitReason = ""; // reset to force a message on next service suspend
-                    Log.Info("Service resuming operation (all parameters in valid ranges).");
-                }
+            if (suspendReasons.Count == 0) {
+                _status.SetSuspended(false, "all parameters in valid ranges");
                 return;
             }
             
             // set state to "Running" if no-one is logged on:
             if (SystemChecks.NoUserIsLoggedOn()) {
-                _status.ServiceSuspended = false;
-                if (!string.IsNullOrEmpty(_status.LimitReason)) {
-                    _status.LimitReason = ""; // reset to force a message on next service suspend
-                    Log.Info("Service resuming operation (no user logged on).");
-                }
+                _status.SetSuspended(false, "no user is currently logged on");
                 return;
             }
 
             // by reaching this point we know the service should be suspended:
-            _status.ServiceSuspended = true;
-            if (limitReason == _status.LimitReason)
-                return;
-            Log.Info("Service suspended due to limitiations [{0}].", limitReason);
-            _status.LimitReason = limitReason;
+            _status.SetSuspended(true, string.Join(", ", suspendReasons));
         }
 
         /// <summary>
diff --git a/ATxTray/AutoTxTray.cs b/ATxTray/AutoTxTray.cs
index fe0b01fa528d7fc9172711f1c9440ef51cfe1e82..eecbc91b0b9f3e678574723f9b795dcf4cb412b9 100644
--- a/ATxTray/AutoTxTray.cs
+++ b/ATxTray/AutoTxTray.cs
@@ -459,18 +459,18 @@ namespace ATxTray
         private void UpdateServiceSuspendedState() {
             // first update the suspend reason as this can possibly change even if the service
             // never leaves the suspended state and we should still display the correct reason:
-            if (_serviceSuspendReason == _status.LimitReason &&
+            if (_serviceSuspendReason == _status.StatusDescription &&
                 _serviceSuspended == _status.ServiceSuspended)
                 return;
 
             _serviceSuspended = _status.ServiceSuspended;
-            _serviceSuspendReason = _status.LimitReason;
+            _serviceSuspendReason = _status.StatusDescription;
             if (_serviceSuspended) {
                 _miSvcSuspended.Text = @"Service suspended, reason: " + _serviceSuspendReason;
                 _miSvcSuspended.BackColor = Color.LightSalmon;
                 /*
                 _notifyIcon.ShowBalloonTip(500, "AutoTx Monitor",
-                    "Service suspended: " + _status.LimitReason, ToolTipIcon.Warning);
+                    "Service suspended: " + _status.StatusDescription, ToolTipIcon.Warning);
                  */
             } else {
                 _miSvcSuspended.Text = @"No limits apply, service active.";
diff --git a/AutoTx.sln b/AutoTx.sln
index 920d1a1d393e6bb2507498f19a8469711a7b4e01..81a8bca68b5a45e515b1ec39b558baced6cbcf2d 100644
--- a/AutoTx.sln
+++ b/AutoTx.sln
@@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 		Resources\Mail-Templates\Transfer-Success.txt = Resources\Mail-Templates\Transfer-Success.txt
 	EndProjectSection
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ATxDiagnostics", "ATxDiagnostics\ATxDiagnostics.csproj", "{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -50,6 +52,10 @@ Global
 		{6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1FDA9634-87C9-4C25-AD12-BF79DA61D44D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE