diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index df76db6..e2fcaf6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,7 @@ releases, in reverse chronological order.
 v3.6.0
 ------
 * Allow passing a custom configuration file with the ``--config/-c`` option.
+* Cached list metadata is now invalidated when it has changed on-disk.
 
 v3.5.0
 ------
diff --git a/todoman/model.py b/todoman/model.py
index 10a71b4..f4e4bda 100644
--- a/todoman/model.py
+++ b/todoman/model.py
@@ -395,7 +395,7 @@ class Cache:
     may be used for filtering/sorting.
     """
 
-    SCHEMA_VERSION = 5
+    SCHEMA_VERSION = 6
 
     def __init__(self, path):
         self.cache_path = str(path)
@@ -445,6 +445,8 @@ class Cache:
                 "name" TEXT PRIMARY KEY,
                 "path" TEXT,
                 "colour" TEXT,
+                "mtime" INTEGER,
+
                 CONSTRAINT path_unique UNIQUE (path)
             );
         '''
@@ -496,7 +498,7 @@ class Cache:
         os.remove(self.cache_path)
         self._conn = None
 
-    def add_list(self, name, path, colour):
+    def add_list(self, name, path, colour, mtime):
         """
         Inserts a new list into the cache.
 
@@ -513,17 +515,25 @@ class Cache:
 
         try:
             self._conn.execute(
-                "INSERT INTO lists (name, path, colour) VALUES (?, ?, ?)",
+                '''
+                INSERT INTO lists (
+                    name,
+                    path,
+                    colour,
+                    mtime
+                ) VALUES (?, ?, ?, ?)
+                ''',
                 (
                     name,
                     path,
                     colour,
+                    mtime,
                 ),
             )
         except sqlite3.IntegrityError as e:
             raise exceptions.AlreadyExists('list', name) from e
 
-        return self.add_list(name, path, colour)
+        return self.add_list(name, path, colour, mtime)
 
     def add_file(self, list_name, path, mtime):
         try:
@@ -824,10 +834,14 @@ class Cache:
         return {l.name: l for l in self.lists()}
 
     def expire_lists(self, paths):
-        results = self._conn.execute("SELECT path, name from lists")
+        results = self._conn.execute("SELECT path, name, mtime from lists")
         for result in results:
             if result['path'] not in paths:
                 self.delete_list(result['name'])
+            else:
+                mtime = paths.get(result['path'])
+                if mtime and mtime > result['mtime']:
+                    self.delete_list(result['name'])
 
     def delete_list(self, name):
         self._conn.execute("DELETE FROM lists WHERE lists.name = ?", (name,))
@@ -894,6 +908,22 @@ class List:
         except (OSError, IOError):
             return split(normpath(path))[1]
 
+    @staticmethod
+    def mtime_for_path(path):
+        colour_file = os.path.join(path, 'color')
+        display_file = os.path.join(path, 'displayname')
+
+        mtimes = []
+        if os.path.exists(colour_file):
+            mtimes.append(_getmtime(colour_file))
+        if os.path.exists(display_file):
+            mtimes.append(_getmtime(display_file))
+
+        if mtimes:
+            return max(mtimes)
+        else:
+            return 0
+
     def __eq__(self, other):
         if isinstance(other, List):
             return self.name == other.name
@@ -918,7 +948,8 @@ class Database:
         self.update_cache()
 
     def update_cache(self):
-        self.cache.expire_lists(self.paths)
+        paths = {path: List.mtime_for_path(path) for path in self.paths}
+        self.cache.expire_lists(paths)
 
         paths_to_mtime = {}
         paths_to_list_name = {}
@@ -928,6 +959,7 @@ class Database:
                 List.name_for_path(path),
                 path,
                 List.colour_for_path(path),
+                paths[path],
             )
             for entry in os.listdir(path):
                 if not entry.endswith('.ics'):

diff --git a/tests/test_model.py b/tests/test_model.py
index 64ed5e9..1b60f39 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -101,6 +101,27 @@ def test_list_colour(tmpdir):
     assert list_.colour == '#8ab6d2'
 
 
+def test_list_colour_cache_invalidation(tmpdir, sleep):
+    tmpdir.join('default').mkdir()
+    with tmpdir.join('default').join('color').open('w') as f:
+        f.write('#8ab6d2')
+
+    db = Database([tmpdir.join('default')], tmpdir.join('cache.sqlite3'))
+    list_ = next(db.lists())
+
+    assert list_.colour == '#8ab6d2'
+
+    sleep()
+
+    with tmpdir.join('default').join('color').open('w') as f:
+        f.write('#f874fd')
+
+    db = Database([tmpdir.join('default')], tmpdir.join('cache.sqlite3'))
+    list_ = next(db.lists())
+
+    assert list_.colour == '#f874fd'
+
+
 def test_list_no_colour(tmpdir):
     tmpdir.join('default').mkdir()
 

List metadata is cached forever
If the metadata (eg: colour) for a cached list changes, we never re-read this. We might just want to save `max(colour_mtime, displayname_mtime)` for lists, and expire their cache entry if that changes.
tests/test_model.py::test_list_colour_cache_invalidation
tests/test_model.py::test_todo_setters tests/test_model.py::test_cached_property_overwriting tests/test_model.py::test_list_equality tests/test_model.py::test_save_recurring_related tests/test_model.py::test_unreadable_ics tests/test_model.py::test_retain_unknown_fields tests/test_model.py::test_retain_tz tests/test_model.py::test_illegal_start_suppression tests/test_model.py::test_complete_recurring[True-None-20990315T020000] tests/test_model.py::test_is_completed tests/test_model.py::test_filename_uid_colision tests/test_model.py::test_todos_startable tests/test_model.py::test_default_status tests/test_model.py::test_querying tests/test_model.py::test_hide_cancelled tests/test_model.py::test_complete_recurring[True-tz0-20990315T020000Z] tests/test_model.py::test_complete_recurring[False-None-20990315T020000] tests/test_model.py::test_complete_recurring[False-tz0-20990315T020000Z] tests/test_model.py::test_cached_property_caching tests/test_model.py::test_clone tests/test_model.py::test_due_date tests/test_model.py::test_nullify_field tests/test_model.py::test_change_paths tests/test_model.py::test_cached_property_property tests/test_model.py::test_todo_filename_absolute_path tests/test_model.py::test_list_colour tests/test_model.py::test_duplicate_list tests/test_model.py::test_list_no_colour tests/test_model.py::test_database_priority_sorting tests/test_model.py::test_list_displayname