diff --git a/changelog.d/17861.bugfix b/changelog.d/17861.bugfix
new file mode 100644
index 0000000000000000000000000000000000000000..abee7a30f7004b3a58337fc39929b3c79a60b266
--- /dev/null
+++ b/changelog.d/17861.bugfix
@@ -0,0 +1 @@
+Fix detection when the built Rust library was outdated when using source installations.
diff --git a/synapse/util/rust.py b/synapse/util/rust.py
index 0e35d6d18812268481d1135b7906ee15b0c4b2a3..37f43459f1c0e09d2a14e9576014c4b24350e275 100644
--- a/synapse/util/rust.py
+++ b/synapse/util/rust.py
@@ -19,9 +19,12 @@
 #
 #
 
+import json
 import os
-import sys
+import urllib.parse
 from hashlib import blake2b
+from importlib.metadata import Distribution, PackageNotFoundError
+from typing import Optional
 
 import synapse
 from synapse.synapse_rust import get_rust_file_digest
@@ -32,22 +35,17 @@ def check_rust_lib_up_to_date() -> None:
     be rebuilt.
     """
 
-    if not _dist_is_editable():
-        return
-
-    synapse_dir = os.path.dirname(synapse.__file__)
-    synapse_root = os.path.abspath(os.path.join(synapse_dir, ".."))
-
-    # Double check we've not gone into site-packages...
-    if os.path.basename(synapse_root) == "site-packages":
-        return
-
-    # ... and it looks like the root of a python project.
-    if not os.path.exists("pyproject.toml"):
-        return
+    # Get the location of the editable install.
+    synapse_root = get_synapse_source_directory()
+    if synapse_root is None:
+        return None
 
     # Get the hash of all Rust source files
-    hash = _hash_rust_files_in_directory(os.path.join(synapse_root, "rust", "src"))
+    rust_path = os.path.join(synapse_root, "rust", "src")
+    if not os.path.exists(rust_path):
+        return None
+
+    hash = _hash_rust_files_in_directory(rust_path)
 
     if hash != get_rust_file_digest():
         raise Exception("Rust module outdated. Please rebuild using `poetry install`")
@@ -82,10 +80,55 @@ def _hash_rust_files_in_directory(directory: str) -> str:
     return hasher.hexdigest()
 
 
-def _dist_is_editable() -> bool:
-    """Is distribution an editable install?"""
-    for path_item in sys.path:
-        egg_link = os.path.join(path_item, "matrix-synapse.egg-link")
-        if os.path.isfile(egg_link):
-            return True
-    return False
+def get_synapse_source_directory() -> Optional[str]:
+    """Try and find the source directory of synapse for editable installs (like
+    those used in development).
+
+    Returns None if not an editable install (or otherwise can't find the source
+    directory).
+    """
+
+    # Try and find the installed matrix-synapse package.
+    try:
+        package = Distribution.from_name("matrix-synapse")
+    except PackageNotFoundError:
+        # The package is not found, so it's not installed and so must be being
+        # pulled out from a local directory (usually the current one).
+        synapse_dir = os.path.dirname(synapse.__file__)
+        synapse_root = os.path.abspath(os.path.join(synapse_dir, ".."))
+
+        # Double check we've not gone into site-packages...
+        if os.path.basename(synapse_root) == "site-packages":
+            return None
+
+        # ... and it looks like the root of a python project.
+        if not os.path.exists("pyproject.toml"):
+            return None
+
+        return synapse_root
+
+    # Read the `direct_url.json` metadata for the package. This won't exist for
+    # packages installed via a repository/etc.
+    # c.f. https://packaging.python.org/en/latest/specifications/direct-url/
+    direct_url_json = package.read_text("direct_url.json")
+    if direct_url_json is None:
+        return None
+
+    # c.f. https://packaging.python.org/en/latest/specifications/direct-url/ for
+    # the format
+    direct_url_dict: dict = json.loads(direct_url_json)
+
+    # `url` must exist as a key, and point to where we fetched the repo from.
+    project_url = urllib.parse.urlparse(direct_url_dict["url"])
+
+    # If its not a local file then we must have built the rust libs either a)
+    # after we downloaded the package, or b) we built the download wheel.
+    if project_url.scheme != "file":
+        return None
+
+    # And finally if its not an editable install then the files can't have
+    # changed since we installed the package.
+    if not direct_url_dict.get("dir_info", {}).get("editable", False):
+        return None
+
+    return project_url.path