diff --git a/ATxCommon/ATxCommon.csproj b/ATxCommon/ATxCommon.csproj
index e75369b8175cc5886616db7c4a857cfab159d4c6..40c19c2f0fd96c3c5e4f53e0b9865daa7618a329 100644
--- a/ATxCommon/ATxCommon.csproj
+++ b/ATxCommon/ATxCommon.csproj
@@ -56,6 +56,7 @@
     <Compile Include="Serializables\ServiceConfig.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Serializables\ServiceStatus.cs" />
+    <Compile Include="StorageStatus.cs" />
     <Compile Include="SystemChecks.cs" />
     <Compile Include="TimeUtils.cs" />
   </ItemGroup>
diff --git a/ATxCommon/StorageStatus.cs b/ATxCommon/StorageStatus.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f8c78e7479c7fd6562db1e3f1e79d38e9086651a
--- /dev/null
+++ b/ATxCommon/StorageStatus.cs
@@ -0,0 +1,161 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using ATxCommon.Serializables;
+using NLog;
+
+namespace ATxCommon
+{
+    public class StorageStatus
+    {
+        /// <summary>
+        /// By default the status will only be updated if more than UpdateDelta seconds have
+        /// elapsed since the last update to prevent too many updates causing high system load.
+        /// </summary>
+        public int UpdateDelta = 20;
+
+        public DateTime LastStatusUpdate = DateTime.MinValue;
+
+        private static readonly Logger Log = LogManager.GetCurrentClassLogger();
+
+        private readonly Dictionary<string, List<DirectoryDetails>> _expiredUserDirs;
+
+        private readonly List<DriveToCheck> _drives;
+        private readonly int _gracePeriod;
+        private readonly string _gracePeriodHuman;
+        private readonly DirectoryInfo _graceLocation;
+
+        /// <summary>
+        /// Initialize the StorageStatus object from the given ServiceConfig.
+        /// </summary>
+        /// <param name="config">The service configuration object.</param>
+        public StorageStatus(ServiceConfig config) {
+            _drives = config.SpaceMonitoring;
+            _gracePeriod = config.GracePeriod;
+            _gracePeriodHuman = config.HumanGracePeriod;
+            _graceLocation = new DirectoryInfo(config.DonePath);
+            _expiredUserDirs = new Dictionary<string, List<DirectoryDetails>>();
+            Update();
+        }
+
+        /// <summary>
+        /// Number of expired directories in the grace location.
+        /// </summary>
+        public int ExpiredUserDirsCount {
+            get {
+                Update();
+                return _expiredUserDirs.Count;
+            }
+        }
+
+        /// <summary>
+        /// Get a dictionary of expired directories from the grace location.
+        /// </summary>
+        public Dictionary<string, List<DirectoryDetails>> ExpiredUserDirs {
+            get {
+                Update();
+                return _expiredUserDirs;
+            }
+        }
+
+        /// <summary>
+        /// Human-friendly summary of expired directories in the grace location.
+        /// </summary>
+        /// <returns>A human-readable (i.e. formatted) string with details on the grace location
+        /// and all expired directories, grouped by the topmost level (i.e. user dirs).</returns>
+        public string ExpiredUserDirsSummary() {
+            Update();
+            var summary = "------ Grace location status, " +
+                          $"threshold: {_gracePeriod} days ({_gracePeriodHuman}) ------\n\n" +
+                          $" - location: [{_graceLocation}]\n";
+
+            if (_expiredUserDirs.Count == 0)
+                return summary + " -- NO EXPIRED folders in grace location! --";
+
+            foreach (var dir in _expiredUserDirs.Keys) {
+                summary += "\n - directory '" + dir + "'\n";
+                foreach (var subdir in _expiredUserDirs[dir]) {
+                    summary += $"    - {subdir.Dir.Name} " +
+                               $"[age: {subdir.HumanAgeFromName}, " +
+                               $"size: {subdir.HumanSize}]\n";
+                }
+            }
+
+            return summary;
+        }
+
+        /// <summary>
+        /// Human-friendly summary of free disk space on all configured drives.
+        /// </summary>
+        /// <param name="onlyLowSpace"></param>
+        /// <returns>A human-readable (i.e. formatted) string with details on the free space on all
+        /// configured drives. If <paramref name="onlyLowSpace"/> is set to "true", space will only
+        /// be reported for drives that are below their corresponding threshold.</returns>
+        public string SpaceSummary(bool onlyLowSpace = false) {
+            Update();
+            var summary = "------ Storage space status ------\n\n";
+            foreach (var drive in _drives) {
+                var msg = $" - drive [{drive.DriveName}] " +
+                          $"free space: {Conv.BytesToString(drive.FreeSpace)} " +
+                          $"(threshold: {Conv.GigabytesToString(drive.SpaceThreshold)})\n";
+
+                if (onlyLowSpace && !drive.DiskSpaceLow()) {
+                    Log.Trace(msg);
+                    continue;
+                }
+
+                summary += msg;
+            }
+
+            return summary;
+        }
+
+        /// <summary>
+        /// Update the current storage status in case the last update is already older than the
+        /// configured threshold <see cref="LastStatusUpdate"/>.
+        /// </summary>
+        /// <param name="force">Update, independently of the last update timestamp.</param>
+        public void Update(bool force = false) {
+            if (force)
+                LastStatusUpdate = DateTime.MinValue;
+
+            if (TimeUtils.SecondsSince(LastStatusUpdate) < UpdateDelta)
+                return;
+
+            foreach (var userdir in _graceLocation.GetDirectories()) {
+                var expired = new List<DirectoryDetails>();
+                foreach (var subdir in userdir.GetDirectories()) {
+                    var dirDetails = new DirectoryDetails(subdir);
+                    if (dirDetails.AgeFromName < _gracePeriod)
+                        continue;
+
+                    expired.Add(dirDetails);
+                }
+                if (expired.Count > 0)
+                    _expiredUserDirs.Add(userdir.Name, expired);
+
+            }
+
+            foreach (var drive in _drives) {
+                try {
+                    drive.FreeSpace = new DriveInfo(drive.DriveName).TotalFreeSpace;
+                }
+                catch (Exception ex) {
+                    // log this as an error which then also gets sent via email (if configured) and
+                    // let the rate-limiter take care of not flooding the admin with mails:
+                    Log.Error("Error in GetFreeDriveSpace({0}): {1}", drive.DriveName, ex.Message);
+                }
+            }
+
+            LastStatusUpdate = DateTime.Now;
+        }
+
+        /// <summary>
+        /// Create an overall storage summary (free space and grace location).
+        /// </summary>
+        /// <returns>Human-readable string with details on free space + grace location.</returns>
+        public string Summary() {
+            return $"{SpaceSummary()}\n{ExpiredUserDirsSummary()}";
+        }
+    }
+}