diff --git a/manager/knot_resolver_manager/datamodel/cache_schema.py b/manager/knot_resolver_manager/datamodel/cache_schema.py
index 783b2c15d5494a681da3e3362a079fb3d7deb093..e40f1e23375e99c185d98fdf3d736f342cad395f 100644
--- a/manager/knot_resolver_manager/datamodel/cache_schema.py
+++ b/manager/knot_resolver_manager/datamodel/cache_schema.py
@@ -1,6 +1,8 @@
-from typing import List, Optional
+from typing import List, Optional, Union
 
-from knot_resolver_manager.datamodel.types import Dir, DomainName, File, SizeUnit, TimeUnit
+from typing_extensions import Literal
+
+from knot_resolver_manager.datamodel.types import Dir, DomainName, File, IntNonNegative, Percent, SizeUnit, TimeUnit
 from knot_resolver_manager.utils.modeling import ConfigSchema
 
 
@@ -25,23 +27,50 @@ class PrefillSchema(ConfigSchema):
             raise ValueError("cache prefilling is not yet supported for non-root zones")
 
 
+class GarbageCollectorSchema(ConfigSchema):
+    """
+    Configuration options of the cache garbage collector (kres-cache-gc).
+
+    ---
+    interval: Time interval how often the garbage collector will be run.
+    threshold: Cache usage in percent that triggers the garbage collector.
+    release: Percent of used cache to be freed by the garbage collector.
+    temp_keys_space: Maximum amount of temporary memory for copied keys (0 = unlimited).
+    rw_deletes: Maximum number of deleted records per read-write transaction (0 = unlimited).
+    rw_reads: Maximum number of readed records per read-write transaction (0 = unlimited).
+    rw_duration: Maximum duration of read-write transaction (0 = unlimited).
+    rw_delay: Wait time between two read-write transactions.
+    dry_run: Run the garbage collector in dry-run mode.
+    """
+
+    interval: TimeUnit = TimeUnit("1s")
+    threshold: Percent = Percent(80)
+    release: Percent = Percent(10)
+    temp_keys_space: SizeUnit = SizeUnit(0)
+    rw_deletes: IntNonNegative = IntNonNegative(100)
+    rw_reads: IntNonNegative = IntNonNegative(200)
+    rw_duration: TimeUnit = TimeUnit(0)
+    rw_delay: TimeUnit = TimeUnit(0)
+    dry_run: bool = False
+
+
 class CacheSchema(ConfigSchema):
     """
     DNS resolver cache configuration.
 
     ---
-    garbage_collector: Automatically use garbage collector to periodically clear cache.
     storage: Cache storage of the DNS resolver.
     size_max: Maximum size of the cache.
+    garbage_collector: Use the garbage collector (kres-cache-gc) to periodically clear cache.
     ttl_min: Minimum time-to-live for the cache entries.
     ttl_max: Maximum time-to-live for the cache entries.
     ns_timeout: Time interval for which a nameserver address will be ignored after determining that it does not return (useful) answers.
     prefill: Prefill the cache periodically by importing zone data obtained over HTTP.
     """
 
-    garbage_collector: bool = True
     storage: Dir = Dir("/var/cache/knot-resolver")
     size_max: SizeUnit = SizeUnit("100M")
+    garbage_collector: Union[GarbageCollectorSchema, Literal[False]] = GarbageCollectorSchema()
     ttl_min: TimeUnit = TimeUnit("5s")
     ttl_max: TimeUnit = TimeUnit("6d")
     ns_timeout: TimeUnit = TimeUnit("1000ms")
diff --git a/manager/knot_resolver_manager/datamodel/local_data_schema.py b/manager/knot_resolver_manager/datamodel/local_data_schema.py
index e10f6c3bdc8477ed227aa2a8d4423848c3b437de..8008e085d2dd9036f78fc23ad4a9e460dc931172 100644
--- a/manager/knot_resolver_manager/datamodel/local_data_schema.py
+++ b/manager/knot_resolver_manager/datamodel/local_data_schema.py
@@ -2,11 +2,12 @@ from typing import Dict, List, Optional, Union
 
 from typing_extensions import Literal
 
-from knot_resolver_manager.datamodel.types import DomainName, IDPattern, IPAddress, TimeUnit, UncheckedPath
-from knot_resolver_manager.utils.modeling import BaseSchema
+from knot_resolver_manager.datamodel.types import DomainName, IDPattern, IPAddress, TimeUnit
+from knot_resolver_manager.datamodel.types.files import UncheckedPath
+from knot_resolver_manager.utils.modeling import ConfigSchema
 
 
-class SubtreeSchema(BaseSchema):
+class SubtreeSchema(ConfigSchema):
     """
     Local data and configuration of subtree.
 
@@ -38,7 +39,7 @@ class SubtreeSchema(BaseSchema):
             raise ValueError("'refresh' can be only configured with 'roots-file' or 'roots-url'")
 
 
-class LocalDataSchema(BaseSchema):
+class LocalDataSchema(ConfigSchema):
     """
     Local data for forward records (A/AAAA) and reverse records (PTR).
 
diff --git a/manager/knot_resolver_manager/datamodel/logging_schema.py b/manager/knot_resolver_manager/datamodel/logging_schema.py
index 1217db233d00f3c9aea8d819c54b9a94591cf563..fb05b8267554d1a31f3037e02e80e862f325ff2b 100644
--- a/manager/knot_resolver_manager/datamodel/logging_schema.py
+++ b/manager/knot_resolver_manager/datamodel/logging_schema.py
@@ -21,6 +21,7 @@ LogTargetEnum = Literal["syslog", "stderr", "stdout"]
 LogGroupsEnum: TypeAlias = Literal[
     "manager",
     "supervisord",
+    "cache-gc",
     "system",
     "cache",
     "io",
diff --git a/manager/knot_resolver_manager/datamodel/types/__init__.py b/manager/knot_resolver_manager/datamodel/types/__init__.py
index bd5bea090610d84ed5d75e4a887b36efa3bfcf5b..427e4a20a2da6e41573a5f0a8132f54d5efb7357 100644
--- a/manager/knot_resolver_manager/datamodel/types/__init__.py
+++ b/manager/knot_resolver_manager/datamodel/types/__init__.py
@@ -18,6 +18,7 @@ from .types import (
     IPv6Address,
     IPv6Network96,
     ListOrSingle,
+    Percent,
     PortNumber,
     SizeUnit,
     TimeUnit,
@@ -44,6 +45,7 @@ __all__ = [
     "IPv6Address",
     "IPv6Network96",
     "ListOrSingle",
+    "Percent",
     "PortNumber",
     "SizeUnit",
     "TimeUnit",
diff --git a/manager/knot_resolver_manager/datamodel/types/base_types.py b/manager/knot_resolver_manager/datamodel/types/base_types.py
index 9bf784026b48bdb0e93d26d366fad01faf56f5bc..96c0a3938ed568979bf04e1bbbd03f77d5dfbab9 100644
--- a/manager/knot_resolver_manager/datamodel/types/base_types.py
+++ b/manager/knot_resolver_manager/datamodel/types/base_types.py
@@ -155,6 +155,9 @@ class UnitBase(IntBase):
                 self._value = int(val) * type(self)._units[unit]
             else:
                 raise ValueError(f"{type(self._value)} Failed to convert: {self}")
+        elif source_value in (0, "0"):
+            self._value_orig = source_value
+            self._value = int(source_value)
         elif isinstance(source_value, int):
             raise ValueError(
                 f"number without units, please convert to string and add unit  - {list(type(self)._units.keys())}",
diff --git a/manager/knot_resolver_manager/datamodel/types/types.py b/manager/knot_resolver_manager/datamodel/types/types.py
index 008b5e92a06fc84e1a05b67c9b13dd920c5e716f..02ef7c7d0a95e6289f18324123bc5cc5d4644b74 100644
--- a/manager/knot_resolver_manager/datamodel/types/types.py
+++ b/manager/knot_resolver_manager/datamodel/types/types.py
@@ -28,6 +28,11 @@ class Int0_65535(IntRangeBase):
     _max: int = 65_535
 
 
+class Percent(IntRangeBase):
+    _min: int = 0
+    _max: int = 100
+
+
 class PortNumber(IntRangeBase):
     _min: int = 1
     _max: int = 65_535
@@ -46,14 +51,20 @@ class SizeUnit(UnitBase):
     def bytes(self) -> int:
         return self._value
 
+    def mbytes(self) -> int:
+        return self._value // 1024 ** 2
+
 
 class TimeUnit(UnitBase):
-    _units = {"ms": 1, "s": 1000, "m": 60 * 1000, "h": 3600 * 1000, "d": 24 * 3600 * 1000}
+    _units = {"us": 1, "ms": 10 ** 3, "s": 10 ** 6, "m": 60 * 10 ** 6, "h": 3600 * 10 ** 6, "d": 24 * 3600 * 10 ** 6}
 
     def seconds(self) -> int:
-        return self._value // 1000
+        return self._value // 1000 ** 2
 
     def millis(self) -> int:
+        return self._value // 1000
+
+    def micros(self) -> int:
         return self._value
 
 
diff --git a/manager/knot_resolver_manager/kres_manager.py b/manager/knot_resolver_manager/kres_manager.py
index 36cddc137c5cdcd111a8678c038030273fc08201..072c73fc31ff1075147587c9b744233712a92aee 100644
--- a/manager/knot_resolver_manager/kres_manager.py
+++ b/manager/knot_resolver_manager/kres_manager.py
@@ -193,7 +193,7 @@ class KresManager:  # pylint: disable=too-many-instance-attributes
                 await self._rolling_restart(config)
                 await self._ensure_number_of_children(config, int(config.workers))
 
-                if self._is_gc_running() != config.cache.garbage_collector:
+                if self._is_gc_running() != bool(config.cache.garbage_collector):
                     if config.cache.garbage_collector:
                         logger.debug("Starting cache GC")
                         await self._start_gc(config)
diff --git a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
index 758a9da5c93065e3c2c1b96cbf6b5c6e478c5984..08450739bb7d6750b85105ac3405ac0d3cf0c170 100644
--- a/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
+++ b/manager/knot_resolver_manager/kresd_controller/supervisord/config_file.py
@@ -48,6 +48,30 @@ class SupervisordKresID(KresID):
             raise RuntimeError(f"Unexpected subprocess type {self.subprocess_type}")
 
 
+def kres_cache_gc_args(config: KresConfig) -> str:
+    args = ""
+
+    if config.logging.level == "debug" or (config.logging.groups and "cache-gc" in config.logging.groups):
+        args += " -v"
+
+    gc_config = config.cache.garbage_collector
+    if gc_config:
+        args += (
+            f" -d {gc_config.interval.millis()}"
+            f" -u {gc_config.threshold}"
+            f" -f {gc_config.release}"
+            f" -l {gc_config.rw_deletes}"
+            f" -L {gc_config.rw_reads}"
+            f" -t {gc_config.temp_keys_space.mbytes()}"
+            f" -m {gc_config.rw_duration.micros()}"
+            f" -w {gc_config.rw_delay.micros()}"
+        )
+        if gc_config.dry_run:
+            args += " -n"
+        return args
+    raise ValueError("missing configuration for the cache garbage collector")
+
+
 @dataclass
 class ProcessTypeConfig:
     """
@@ -66,7 +90,7 @@ class ProcessTypeConfig:
         return ProcessTypeConfig(  # type: ignore[call-arg]
             logfile=supervisord_subprocess_log_dir(config) / "gc.log",
             workdir=cwd,
-            command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)} -d 1000",
+            command=f"{kres_gc_executable()} -c {kresd_cache_dir(config)}{kres_cache_gc_args(config)}",
             environment="",
         )
 
@@ -152,6 +176,7 @@ async def write_config_file(config: KresConfig) -> None:
         manager=ProcessTypeConfig.create_manager_config(config),
         config=SupervisordConfig.create(config),
     )
+    print(config_string)
     await writefile(supervisord_config_file_tmp(config), config_string)
     # atomically replace (we don't technically need this right now, but better safe then sorry)
     os.rename(supervisord_config_file_tmp(config), supervisord_config_file(config))
diff --git a/manager/tests/unit/datamodel/types/test_custom_types.py b/manager/tests/unit/datamodel/types/test_custom_types.py
index 5fba82eedede622a35c98e16216d828535ea39a8..b9d6f56723fb56ba883c084ebaad4d1da5a8d060 100644
--- a/manager/tests/unit/datamodel/types/test_custom_types.py
+++ b/manager/tests/unit/datamodel/types/test_custom_types.py
@@ -56,13 +56,14 @@ def test_size_unit_invalid(val: Any):
         SizeUnit(val)
 
 
-@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms"])
+@pytest.mark.parametrize("val", ["1d", "24h", "1440m", "86400s", "86400000ms", "86400000000us"])
 def test_time_unit_valid(val: str):
     o = TimeUnit(val)
-    assert int(o) == 86400000
+    assert int(o) == 86400000000
     assert str(o) == val
     assert o.seconds() == 86400
     assert o.millis() == 86400000
+    assert o.micros() == 86400000000
 
 
 @pytest.mark.parametrize("val", ["-1", "-24h", "1440mm", 6575, -1440])