diff --git a/docs/source/route_layer/sync.rst b/docs/source/route_layer/sync.rst
index efb1bebe91f13f22c518a2dbb96ae274394e7d78..aa04ea2e36d3272961791cdbe8f86748649672fe 100644
--- a/docs/source/route_layer/sync.rst
+++ b/docs/source/route_layer/sync.rst
@@ -72,5 +72,35 @@ When initializing the `PineconeIndex` object, we can specify the `sync` paramete
 Checking for Synchronization
 ----------------------------
 
-To verify whether the local and remote instances are synchronized, you can use the `is_synced` method. This method checks if the routes, utterances, and associated metadata in the local instance match those stored in the remote index.
-Consider that if the `sync` flag is not set (e.g. for indexes different from Pinecone), it raises an error. If the index supports sync feature and everything aligns, it returns `True`, indicating that the local and remote instances are synchronized, otherwise it returns `False`.
\ No newline at end of file
+To verify whether the local and remote instances are synchronized, you can use
+the `RouteLayer.is_synced` method. This method checks if the routes, utterances,
+and associated metadata in the local instance match those stored in the remote
+index.
+
+The `is_synced` method works in two steps. The first is our *fast* sync check.
+The fast check creates a hash of our local route layer which is constructed
+from:
+
+- `encoder_type` and `encoder_name`
+- `route` names
+- `route` utterances
+- `route` description
+- `route` function schemas (if any)
+- `route` llm (if any)
+- `route` score threshold
+- `route` metadata (if any)
+
+The fast check then compares this hash to the hash of the remote index. If
+the hashes match, we know that the local and remote instances are synchronized
+and we can return `True`. If the hashes do not match, we need to perform a
+*slow* sync check.
+
+The slow sync check works by creating a `LayerConfig` object from the remote
+index and then comparing this to our local `LayerConfig` object. If the two
+objects match, we know that the local and remote instances are synchronized and
+we can return `True`. If the two objects do not match, we need to perform a
+diff.
+
+The diff works by creating a list of all the routes in the remote index and
+then comparing these to the routes in our local instance. Any differences
+between the remote and local routes are shown in the diff.
\ No newline at end of file
diff --git a/semantic_router/index/base.py b/semantic_router/index/base.py
index 271120219ccf3898a3f8e3491d8cf44582926017..bd5c7c943312eead08d313b9415690b252baacb4 100644
--- a/semantic_router/index/base.py
+++ b/semantic_router/index/base.py
@@ -4,6 +4,7 @@ import numpy as np
 from pydantic.v1 import BaseModel
 
 from semantic_router.schema import ConfigParameter
+from semantic_router.route import Route
 
 
 class BaseIndex(BaseModel):
@@ -37,16 +38,33 @@ class BaseIndex(BaseModel):
         """
         raise NotImplementedError("This method should be implemented by subclasses.")
 
-    def get_routes(self):
-        """
-        Retrieves a list of routes and their associated utterances from the index.
-        This method should be implemented by subclasses.
-
-        :returns: A list of tuples, each containing a route name and an associated utterance.
-        :rtype: list[tuple]
-        :raises NotImplementedError: If the method is not implemented by the subclass.
-        """
-        raise NotImplementedError("This method should be implemented by subclasses.")
+    def get_routes(self) -> List[Route]:
+        """Gets a list of route objects currently stored in the index.
+
+        :return: A list of Route objects.
+        :rtype: List[Route]
+        """
+        route_tuples = self.get_utterances()
+        routes_dict: Dict[str, List[str]] = {}
+        # first create a dictionary of routes mapping to all their utterances,
+        # function_schema, and metadata
+        for route_name, utterance, function_schema, metadata in route_tuples:
+            routes_dict.setdefault(
+                route_name,
+                {
+                    "function_schemas": None,
+                    "metadata": {},
+                },
+            )
+            routes_dict[route_name]["utterances"] = routes_dict[route_name].get(
+                "utterances", []
+            )
+            routes_dict[route_name]["utterances"].append(utterance)
+        # then create a list of routes from the dictionary
+        routes: List[Route] = []
+        for route_name, route_data in routes_dict.items():
+            routes.append(Route(name=route_name, **route_data))
+        return routes
 
     def _remove_and_sync(self, routes_to_delete: dict):
         """
diff --git a/semantic_router/index/local.py b/semantic_router/index/local.py
index 09e23ffc1b6fb7f152cd4e6aad12ca5a8df89391..52e0c0b693bf20c035a59105a0989c2801e0f058 100644
--- a/semantic_router/index/local.py
+++ b/semantic_router/index/local.py
@@ -60,7 +60,7 @@ class LocalIndex(BaseIndex):
         if self.sync is not None:
             logger.error("Sync remove is not implemented for LocalIndex.")
 
-    def get_routes(self) -> List[Tuple]:
+    def get_utterances(self) -> List[Tuple]:
         """
         Gets a list of route and utterance objects currently stored in the index.
 
diff --git a/semantic_router/index/pinecone.py b/semantic_router/index/pinecone.py
index 2aae42b873d76d60ecdfd1da14ebf63bd8b9d8d3..25d2f5d912976d9b20d517d6c724ad950b2c3310 100644
--- a/semantic_router/index/pinecone.py
+++ b/semantic_router/index/pinecone.py
@@ -218,6 +218,7 @@ class PineconeIndex(BaseIndex):
             logger.warning("Index could not be initialized.")
         self.host = index_stats["host"] if index_stats else None
 
+    # TODO: deprecate?
     def _format_routes_dict_for_sync(
         self,
         local_route_names: List[str],
@@ -256,48 +257,6 @@ class PineconeIndex(BaseIndex):
 
         return local_dict, remote_dict
 
-    def is_synced(
-        self,
-        local_route_names: List[str],
-        local_utterances_list: List[str],
-        local_function_schemas_list: List[Dict[str, Any]],
-        local_metadata_list: List[Dict[str, Any]],
-    ) -> bool:
-        remote_routes = self.get_routes()
-
-        local_dict, remote_dict = self._format_routes_dict_for_sync(
-            local_route_names,
-            local_utterances_list,
-            local_function_schemas_list,
-            local_metadata_list,
-            remote_routes,
-        )
-        logger.info(f"LOCAL: {local_dict}")
-        logger.info(f"REMOTE: {remote_dict}")
-
-        all_routes = set(remote_dict.keys()).union(local_dict.keys())
-
-        for route in all_routes:
-            local_utterances = local_dict.get(route, {}).get("utterances", set())
-            remote_utterances = remote_dict.get(route, {}).get("utterances", set())
-            local_function_schemas = (
-                local_dict.get(route, {}).get("function_schemas", {}) or {}
-            )
-            remote_function_schemas = (
-                remote_dict.get(route, {}).get("function_schemas", {}) or {}
-            )
-            local_metadata = local_dict.get(route, {}).get("metadata", {})
-            remote_metadata = remote_dict.get(route, {}).get("metadata", {})
-
-            if (
-                local_utterances != remote_utterances
-                or local_function_schemas != remote_function_schemas
-                or local_metadata != remote_metadata
-            ):
-                return False
-
-        return True
-
     def _sync_index(
         self,
         local_route_names: List[str],
@@ -310,7 +269,7 @@ class PineconeIndex(BaseIndex):
             self.dimensions = self.dimensions or dimensions
             self.index = self._init_index(force_create=True)
 
-        remote_routes = self.get_routes()
+        remote_routes = self.get_utterances()
 
         local_dict, remote_dict = self._format_routes_dict_for_sync(
             local_route_names,
@@ -602,7 +561,7 @@ class PineconeIndex(BaseIndex):
 
         return all_vector_ids, metadata
 
-    def get_routes(self) -> List[Tuple]:
+    def get_utterances(self) -> List[Tuple]:
         """Gets a list of route and utterance objects currently stored in the
         index, including additional metadata.
 
@@ -680,15 +639,16 @@ class PineconeIndex(BaseIndex):
     def _read_hash(self) -> ConfigParameter:
         if self.index is None:
             raise ValueError("Index has not been initialized.")
+        hash_id = f"sr_hash#{self.namespace}"
         hash_record = self.index.fetch(
-            ids=[f"sr_hash#{self.namespace}"],
+            ids=[hash_id],
             namespace="sr_config",
         )
         if hash_record["vectors"]:
             return ConfigParameter(
                 field="sr_hash",
-                value=hash_record["vectors"]["sr_hash"]["metadata"]["value"],
-                created_at=hash_record["vectors"]["sr_hash"]["metadata"]["created_at"],
+                value=hash_record["vectors"][hash_id]["metadata"]["value"],
+                created_at=hash_record["vectors"][hash_id]["metadata"]["created_at"],
                 namespace=self.namespace,
             )
         else:
diff --git a/semantic_router/index/postgres.py b/semantic_router/index/postgres.py
index ff63ec09c419a787033a8412a3517fa1bcb49d58..e0a8b872253095d0763a6ca77d43d906ef70a447 100644
--- a/semantic_router/index/postgres.py
+++ b/semantic_router/index/postgres.py
@@ -423,17 +423,17 @@ class PostgresIndex(BaseIndex):
 
         return all_vector_ids, metadata
 
-    def get_routes(self) -> List[Tuple]:
+    def get_utterances(self) -> List[Tuple]:
         """
         Gets a list of route and utterance objects currently stored in the index.
 
-        :return: A list of (route_name, utterance) tuples.
+        :return: A list of (route_name, utterance, function_schema, metadata) tuples.
         :rtype: List[Tuple]
         """
         # Get all records with metadata
         _, metadata = self._get_all(include_metadata=True)
-        # Create a list of (route_name, utterance) tuples
-        route_tuples = [(x["sr_route"], x["sr_utterance"]) for x in metadata]
+        # Create a list of (route_name, utterance, function_schema, metadata) tuples
+        route_tuples = [(x["sr_route"], x["sr_utterance"], None, {}) for x in metadata]
         return route_tuples
 
     def delete_all(self):
@@ -463,7 +463,7 @@ class PostgresIndex(BaseIndex):
             self.conn.commit()
 
     def aget_routes(self):
-        logger.error("Sync remove is not implemented for PostgresIndex.")
+        raise NotImplementedError("Sync remove is not implemented for PostgresIndex.")
 
     def __len__(self):
         """
diff --git a/semantic_router/index/qdrant.py b/semantic_router/index/qdrant.py
index 49e0d0d4f66e9d3fbb5e3cc3bfe0eb0c94402726..1026868849ffbe83eb4c05f847315fdafde97ad9 100644
--- a/semantic_router/index/qdrant.py
+++ b/semantic_router/index/qdrant.py
@@ -199,12 +199,12 @@ class QdrantIndex(BaseIndex):
             batch_size=batch_size,
         )
 
-    def get_routes(self) -> List[Tuple]:
+    def get_utterances(self) -> List[Tuple]:
         """
         Gets a list of route and utterance objects currently stored in the index.
 
         Returns:
-            List[Tuple]: A list of (route_name, utterance) objects.
+            List[Tuple]: A list of (route_name, utterance, function_schema, metadata) objects.
         """
 
         from qdrant_client import grpc
@@ -228,7 +228,12 @@ class QdrantIndex(BaseIndex):
             results.extend(records)
 
         route_tuples = [
-            (x.payload[SR_ROUTE_PAYLOAD_KEY], x.payload[SR_UTTERANCE_PAYLOAD_KEY])
+            (
+                x.payload[SR_ROUTE_PAYLOAD_KEY],
+                x.payload[SR_UTTERANCE_PAYLOAD_KEY],
+                None,
+                {},
+            )
             for x in results
         ]
         return route_tuples
diff --git a/semantic_router/layer.py b/semantic_router/layer.py
index e2f7f2667a890957222d325d14d3a8d9fa3aa41a..8637dfdf54843e50c5b37498e8efa1c7e31969dc 100644
--- a/semantic_router/layer.py
+++ b/semantic_router/layer.py
@@ -1,3 +1,4 @@
+from difflib import Differ
 import importlib
 import json
 import os
@@ -123,6 +124,68 @@ class LayerConfig:
                 encoder_type=encoder_type, encoder_name=encoder_name, routes=routes
             )
 
+    @classmethod
+    def from_tuples(
+        cls,
+        route_tuples: List[Tuple[str, str]],
+        encoder_type: str = "openai",
+        encoder_name: Optional[str] = None,
+    ):
+        """Initialize a LayerConfig from a list of tuples of routes and
+        utterances.
+
+        :param route_tuples: A list of tuples, each containing a route name and an
+            associated utterance.
+        :type route_tuples: List[Tuple[str, str]]
+        :param encoder_type: The type of encoder to use, defaults to "openai".
+        :type encoder_type: str, optional
+        :param encoder_name: The name of the encoder to use, defaults to None.
+        :type encoder_name: Optional[str], optional
+        """
+        routes: List[Route] = []
+        routes_dict: Dict[str, List[str]] = {}
+        # first create a dictionary of routes mapping to all their utterances,
+        # function_schema, and metadata
+        for route_name, utterance, function_schema, metadata in route_tuples:
+            routes_dict.setdefault(
+                route_name,
+                {
+                    "function_schemas": None,
+                    "metadata": {},
+                },
+            )
+            routes_dict[route_name]["utterances"] = routes_dict[route_name].get(
+                "utterances", []
+            )
+            routes_dict[route_name]["utterances"].append(utterance)
+        # then create a list of routes from the dictionary
+        for route_name, route_data in routes_dict.items():
+            routes.append(Route(name=route_name, **route_data))
+        return cls(routes=routes, encoder_type=encoder_type, encoder_name=encoder_name)
+
+    @classmethod
+    def from_index(
+        cls,
+        index: BaseIndex,
+        encoder_type: str = "openai",
+        encoder_name: Optional[str] = None,
+    ):
+        """Initialize a LayerConfig from a BaseIndex object.
+
+        :param index: The index to initialize the LayerConfig from.
+        :type index: BaseIndex
+        :param encoder_type: The type of encoder to use, defaults to "openai".
+        :type encoder_type: str, optional
+        :param encoder_name: The name of the encoder to use, defaults to None.
+        :type encoder_name: Optional[str], optional
+        """
+        remote_routes = index.get_utterances()
+        return cls.from_tuples(
+            route_tuples=remote_routes,
+            encoder_type=encoder_type,
+            encoder_name=encoder_name,
+        )
+
     def to_dict(self) -> Dict[str, Any]:
         return {
             "encoder_type": self.encoder_type,
@@ -153,6 +216,30 @@ class LayerConfig:
             elif ext in [".yaml", ".yml"]:
                 yaml.safe_dump(self.to_dict(), f)
 
+    def _get_diff(self, other: "LayerConfig") -> List[Dict[str, Any]]:
+        """Get the difference between two LayerConfigs.
+
+        :param other: The LayerConfig to compare to.
+        :type other: LayerConfig
+        :return: A list of differences between the two LayerConfigs.
+        :rtype: List[Dict[str, Any]]
+        """
+        self_yaml = yaml.dump(self.to_dict())
+        other_yaml = yaml.dump(other.to_dict())
+        differ = Differ()
+        return list(differ.compare(self_yaml.splitlines(), other_yaml.splitlines()))
+
+    def show_diff(self, other: "LayerConfig") -> str:
+        """Show the difference between two LayerConfigs.
+
+        :param other: The LayerConfig to compare to.
+        :type other: LayerConfig
+        :return: A string showing the difference between the two LayerConfigs.
+        :rtype: str
+        """
+        diff = self._get_diff(other)
+        return "\n".join(diff)
+
     def add(self, route: Route):
         self.routes.append(route)
         logger.info(f"Added route `{route.name}`")
@@ -499,7 +586,7 @@ class RouteLayer:
         """Pulls out the latest routes from the index."""
         raise NotImplementedError("This method has not yet been implemented.")
         route_mapping = {route.name: route for route in self.routes}
-        index_routes = self.index.get_routes()
+        index_routes = self.index.get_utterances()
         new_routes_names = []
         new_routes = []
         for route_name, utterance in index_routes:
@@ -548,18 +635,45 @@ class RouteLayer:
         remote_hash = self.index._read_hash()
         if local_hash.value == remote_hash.value:
             return True
-        # TODO: we may be able to remove the below logic
-        # if hashes are different, double check
-        local_route_names, local_utterances, local_function_schemas, local_metadata = (
-            self._extract_routes_details(self.routes, include_metadata=True)
-        )
-        # return result of double check
-        return self.index.is_synced(
-            local_route_names=local_route_names,
-            local_utterances_list=local_utterances,
-            local_function_schemas_list=local_function_schemas,
-            local_metadata_list=local_metadata,
+        else:
+            return False
+
+    def get_utterance_diff(self) -> List[str]:
+        """Get the difference between the local and remote utterances. Returns
+        a list of strings showing what is different in the remote when compared
+        to the local. For example:
+
+        ["  route1: utterance1",
+         "  route1: utterance2",
+         "- route2: utterance3",
+         "- route2: utterance4"]
+
+        Tells us that the remote is missing "route2: utterance3" and "route2:
+        utterance4", which do exist locally. If we see:
+
+        ["  route1: utterance1",
+         "  route1: utterance2",
+         "+ route2: utterance3",
+         "+ route2: utterance4"]
+
+        This diff tells us that the remote has "route2: utterance3" and
+        "route2: utterance4", which do not exist locally.
+        """
+        # first we get remote and local utterances
+        remote_utterances = [f"{x[0]}: {x[1]}" for x in self.index.get_utterances()]
+        local_routes, local_utterance_arr, _ = self._extract_routes_details(
+            self.routes, include_metadata=False
         )
+        local_utterances = [
+            f"{x[0]}: {x[1]}" for x in zip(local_routes, local_utterance_arr)
+        ]
+        # sort local and remote utterances
+        local_utterances.sort()
+        remote_utterances.sort()
+        # now get diff
+        differ = Differ()
+        diff = list(differ.compare(local_utterances, remote_utterances))
+        return diff
 
     def _add_and_sync_routes(self, routes: List[Route]):
         # create embeddings for all routes and sync at startup with remote ones based on sync setting
@@ -829,7 +943,7 @@ class RouteLayer:
             # Switch to a local index for fitting
             from semantic_router.index.local import LocalIndex
 
-            remote_routes = self.index.get_routes()
+            remote_routes = self.index.get_utterances()
             # TODO Enhance by retrieving directly the vectors instead of embedding all utterances again
             routes, utterances, function_schemas, metadata = map(
                 list, zip(*remote_routes)
diff --git a/semantic_router/route.py b/semantic_router/route.py
index 41fd0bf2c4e2b496b92f22f841c8b565dda8bdad..50d516fa5bbe31d7eff25b8ece8372801649abbd 100644
--- a/semantic_router/route.py
+++ b/semantic_router/route.py
@@ -99,9 +99,6 @@ class Route(BaseModel):
             func_call = None
         return RouteChoice(name=self.name, function_call=func_call)
 
-    # def to_dict(self) -> Dict[str, Any]:
-    #     return self.dict()
-
     def to_dict(self) -> Dict[str, Any]:
         data = self.dict()
         if self.llm is not None:
diff --git a/semantic_router/schema.py b/semantic_router/schema.py
index bbd5dc1bd02b40d6c68294c1441a4dfd89cf1c5a..8d87f01733f0e7803d5b38c6359744a66790ba54 100644
--- a/semantic_router/schema.py
+++ b/semantic_router/schema.py
@@ -80,7 +80,7 @@ class ConfigParameter(BaseModel):
             "metadata": {
                 "value": self.value,
                 "created_at": self.created_at,
-                "namespace": self.namespace,
+                "namespace": namespace,
                 "field": self.field,
             },
         }
diff --git a/tests/unit/test_layer.py b/tests/unit/test_layer.py
index 3bfcd485cc34389905ab161bcfa2c79cb12b4119..f39639451448e166f58339464022797802551e84 100644
--- a/tests/unit/test_layer.py
+++ b/tests/unit/test_layer.py
@@ -252,14 +252,14 @@ class TestRouteLayer:
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
         assert route_layer.routes == [routes[0]]
         assert route_layer.index is not None
-        assert len(route_layer.index.get_routes()) == 2
+        assert len(route_layer.index.get_utterances()) == 2
 
         # Add route2 and check
         route_layer.add(route=routes[1])
         if index_cls is PineconeIndex:
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
         assert route_layer.routes == [routes[0], routes[1]]
-        assert len(route_layer.index.get_routes()) == 5
+        assert len(route_layer.index.get_utterances()) == 5
 
     def test_list_route_names(self, openai_encoder, routes, index_cls):
         index = init_index(index_cls)
@@ -306,7 +306,7 @@ class TestRouteLayer:
         if index_cls is PineconeIndex:
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
         assert route_layer.index is not None
-        assert len(route_layer.index.get_routes()) == 5
+        assert len(route_layer.index.get_utterances()) == 5
 
     def test_query_and_classification(self, openai_encoder, routes, index_cls):
         index = init_index(index_cls, dimensions=3)
@@ -381,7 +381,7 @@ class TestRouteLayer:
                 encoder=openai_encoder, routes=routes_2, index=pinecone_index
             )
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
-            assert route_layer.index.get_routes() == [
+            assert route_layer.index.get_utterances() == [
                 ("Route 1", "Hello", None, {}),
                 ("Route 2", "Hi", None, {}),
             ], "The routes in the index should match the local routes"
@@ -393,7 +393,7 @@ class TestRouteLayer:
             )
 
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
-            assert route_layer.index.get_routes() == [
+            assert route_layer.index.get_utterances() == [
                 ("Route 1", "Hello", None, {}),
                 ("Route 2", "Hi", None, {}),
             ], "The routes in the index should match the local routes"
@@ -405,7 +405,7 @@ class TestRouteLayer:
             )
 
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
-            assert route_layer.index.get_routes() == [
+            assert route_layer.index.get_utterances() == [
                 ("Route 1", "Hello", None, {}),
                 ("Route 2", "Hi", None, {}),
             ], "The routes in the index should match the local routes"
@@ -417,7 +417,7 @@ class TestRouteLayer:
             )
 
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
-            assert route_layer.index.get_routes() == [
+            assert route_layer.index.get_utterances() == [
                 ("Route 1", "Hello", None, {"type": "default"}),
                 ("Route 1", "Hi", None, {"type": "default"}),
                 ("Route 2", "Bye", None, {}),
@@ -432,7 +432,7 @@ class TestRouteLayer:
             )
 
             time.sleep(PINECONE_SLEEP)  # allow for index to be populated
-            assert route_layer.index.get_routes() == [
+            assert route_layer.index.get_utterances() == [
                 ("Route 1", "Hello", None, {"type": "default"}),
                 ("Route 1", "Hi", None, {"type": "default"}),
                 ("Route 1", "Goodbye", None, {"type": "default"}),