diff --git a/api/__init__.py b/api/__init__.py
index 42b11e5..3592098 100644
--- a/api/__init__.py
+++ b/api/__init__.py
@@ -14,7 +14,8 @@ from .wireguard import WireGuard
 
 login_path = '/login'
 callback_path = '/auth'
-public_paths = [login_path, callback_path]
+cron_path = '/cron'
+public_paths = [login_path, callback_path, cron_path]
 user_cookie = 'username'
 token_cookie = 'token'
 access_cookie = 'access'
@@ -119,6 +120,12 @@ def authorize():
     return response
 
 
+@app.route(cron_path)
+def run_cron():
+    app.wg.run_cleanup()
+    return Response()
+
+
 @app.route('/configs/')
 def list_configs() -> list:
     return app.wg.list_configs()
diff --git a/api/exceptions.py b/api/exceptions.py
index 0520ca2..e995594 100644
--- a/api/exceptions.py
+++ b/api/exceptions.py
@@ -1,3 +1,7 @@
 class ClientLimitError(Exception):
     def __init__(self, message):
         self.message = message
+
+class ValiditySpecificationError(Exception):
+    def __init__(self, message):
+        self.message = message
diff --git a/api/wireguard.py b/api/wireguard.py
index 12f4fc0..befdec3 100644
--- a/api/wireguard.py
+++ b/api/wireguard.py
@@ -7,7 +7,9 @@ import ipaddress
 import json
 import subprocess
 
-from .exceptions import ClientLimitError
+from dateutil.relativedelta import relativedelta
+
+from .exceptions import ClientLimitError, ValiditySpecificationError
 
 
 confsuffix = '.conf'
@@ -127,6 +129,24 @@ def delete_route(client_ip: str) -> None:
     # from the client config
     run_command('del', client_ip)
 
+def parse_timestring(spec: str) -> relativedelta:
+    count, unit = spec.split()
+    try:
+        count = int(count)
+    except Exception:
+        raise ValiditySpecificationError(
+            f"'{spec}' is not recognized as a valid time specification")
+    if unit == 'year' or unit == 'years':
+        return relativedelta(years=count)
+    if unit == 'month' or unit == 'months':
+        return relativedelta(months=count)
+    if unit == 'week' or unit == 'weeks':
+        return relativedelta(weeks=count)
+    if unit == 'day' or unit == 'days':
+        return relativedelta(days=count)
+    raise ValiditySpecificationError(
+        f"'{spec}' is not recognized as a valid time specification")
+
 
 class WireGuard:
     def __init__(self, config: dict):
@@ -137,6 +157,12 @@ class WireGuard:
         self.configs_base = Path(config['configs_base'])
         self.max_clients = config.getint('user_client_limit', fallback=0)
 
+        client_validity = config.get('user_client_validity', fallback=0)
+        if client_validity:
+            self.client_validity = parse_timestring(client_validity)
+        else:
+            self.client_validity = 0
+
         self.server_config_base = None
         if 'server_extra_config' in config:
             self.server_config_base = Path(config['server_extra_config'])
@@ -278,10 +304,15 @@ class WireGuard:
         self.wg_updated = True
         return client_ip
 
-    def update_config(self,
-                      config_id: str,
-                      name: str,
-                      description: str) -> None:
+    def update_config(self, *args) -> None:
+        call_with_lock(self._unsafe_update_config,
+                       args,
+                       10)
+
+    def _unsafe_update_config(self,
+                              config_id: str,
+                              name: str,
+                              description: str) -> None:
         with open(self.meta_filepath(config_id), 'r+') as mf:
             metadata = json.load(mf)
             metadata['name'] = name
@@ -301,7 +332,12 @@ class WireGuard:
                 'created': metadata['created'],
                 'data': configdata}
 
-    def delete_config(self, config_id: str) -> None:
+    def delete_config(self, *args) -> None:
+        call_with_lock(self._unsafe_delete_config,
+                       args,
+                       10)
+
+    def _unsafe_delete_config(self, config_id: str) -> None:
         config_path = self.config_filepath(config_id)
         paths = [config_path,
                  self.serverconfig_filepath(config_id),
@@ -320,6 +356,39 @@ class WireGuard:
         self.log(f'{self.user_name}/{config_id}', 'Deleted config')
         self.wg_updated = True
 
+    def delete_many_configs(self, *args) -> None:
+        call_with_lock(self._unsafe_delete_many_configs,
+                       args,
+                       10)
+
+    def _unsafe_delete_many_configs(self, expired: dict) -> None:
+        for user_name, configs in expired.items():
+            self.set_user(user_name)
+            for config_id in configs:
+                self._unsafe_delete_config(config_id)
+
+    def run_cleanup(self) -> None:
+        if not self.client_validity:
+            return
+
+        now = datetime.now()
+        expired = {}
+
+        # Collect expired configs
+        for metafile in self.configs_base.glob('*/*'+metasuffix):
+            with open(metafile, 'r') as cf:
+                metadata = json.load(cf)
+            created = datetime.fromisoformat(metadata['created'])
+            config_id = metafile.stem
+            user_name = metafile.parent.name
+            if now > created + self.client_validity:
+                if user_name not in expired:
+                    expired[user_name] = []
+                expired[user_name].append(config_id)
+
+        # Delete the expired configs in a separate step to minimize lock time
+        self.delete_many_configs(expired)
+
     def update(self) -> None:
         if not self.wg_updated:
             return
diff --git a/config.ini.example b/config.ini.example
index 6694d59..2611cd3 100644
--- a/config.ini.example
+++ b/config.ini.example
@@ -45,6 +45,13 @@ client_extra_config = path/to/another/fragment
 # Defaults to unlimited, equivalent to setting this value to 0.
 user_client_limit = 3
 
+# Optional:
+# The amount of time a client is valid after creation.
+# Accepts strings of the format "N <time-units>", where time-units
+# is days, weeks, months or years.
+# Defaults to unlimited, equivalent to setting this value to 0.
+user_client_validity = 0
+
 
 [security]
 # Optional.
diff --git a/requirements.txt b/requirements.txt
index 30692b7..7aa1ec5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 flask
 requests
+python-dateutil