diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php
index dbf605523d..ff03765b43 100644
--- a/src/wp-includes/rest-api/class-wp-rest-server.php
+++ b/src/wp-includes/rest-api/class-wp-rest-server.php
@@ -87,6 +87,14 @@ class WP_REST_Server {
 	 */
 	protected $embed_cache = array();
 
+	/**
+	 * Cached route maps keyed by namespace.
+	 *
+	 * @since 6.9.0
+	 * @var array
+	 */
+	protected $route_map_cache = array();
+
 	/**
 	 * Stores request objects that are currently being handled.
 	 *
@@ -924,6 +932,9 @@ class WP_REST_Server {
 		} else {
 			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
 		}
+
+		// Invalidate the route map cache when routes change.
+		$this->route_map_cache = array();
 	}
 
 	/**
@@ -949,6 +960,12 @@ class WP_REST_Server {
 	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
 	 */
 	public function get_routes( $route_namespace = '' ) {
+		$cache_key = $route_namespace ? $route_namespace : '';
+
+		if ( isset( $this->route_map_cache[ $cache_key ] ) ) {
+			return $this->route_map_cache[ $cache_key ];
+		}
+
 		$endpoints = $this->endpoints;
 
 		if ( $route_namespace ) {
@@ -1016,6 +1033,8 @@ class WP_REST_Server {
 			}
 		}
 
+		$this->route_map_cache[ $cache_key ] = $endpoints;
+
 		return $endpoints;
 	}
 
diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php
index 57b7bbb38a..92770e5c04 100644
--- a/tests/phpunit/tests/rest-api/rest-server.php
+++ b/tests/phpunit/tests/rest-api/rest-server.php
@@ -2012,6 +2012,82 @@ class Tests_REST_Server extends WP_Test_REST_TestCase {
 		$this->assertSame( 204, $response->get_status(), '/test-ns/v1/test' );
 	}
 
+	/**
+	 * @ticket 39473
+	 */
+	public function test_get_routes_caches_result() {
+		$filter_count = 0;
+		$counting_filter = static function ( $endpoints ) use ( &$filter_count ) {
+			++$filter_count;
+			return $endpoints;
+		};
+
+		add_filter( 'rest_endpoints', $counting_filter );
+
+		$server = rest_get_server();
+		$server->get_routes();
+		$server->get_routes();
+		$server->get_routes();
+
+		remove_filter( 'rest_endpoints', $counting_filter );
+
+		$this->assertSame( 1, $filter_count, 'The rest_endpoints filter should only fire once when get_routes() is called multiple times.' );
+	}
+
+	/**
+	 * @ticket 39473
+	 */
+	public function test_get_routes_cache_is_keyed_by_namespace() {
+		$server = rest_get_server();
+
+		$all_routes       = $server->get_routes();
+		$namespaced_routes = $server->get_routes( 'oembed/1.0' );
+
+		$this->assertNotEquals( $all_routes, $namespaced_routes, 'Cached routes for different namespaces should differ.' );
+
+		foreach ( $namespaced_routes as $route => $handlers ) {
+			$this->assertStringStartsWith( '/oembed/1.0', $route );
+		}
+	}
+
+	/**
+	 * @ticket 39473
+	 */
+	public function test_get_routes_cache_invalidated_on_register() {
+		$server = rest_get_server();
+
+		$routes_before = $server->get_routes();
+
+		register_rest_route(
+			'test-cache-ns',
+			'/test-cache',
+			array(
+				'methods'             => array( 'GET' ),
+				'callback'            => '__return_true',
+				'permission_callback' => '__return_true',
+			)
+		);
+
+		$routes_after = $server->get_routes();
+
+		$this->assertArrayNotHasKey( '/test-cache-ns/test-cache', $routes_before );
+		$this->assertArrayHasKey( '/test-cache-ns/test-cache', $routes_after );
+	}
+
+	/**
+	 * @ticket 39473
+	 */
+	public function test_get_routes_cached_result_matches_uncached() {
+		$server = rest_get_server();
+
+		// First call populates cache.
+		$first_call = $server->get_routes();
+		// Second call returns from cache.
+		$second_call = $server->get_routes();
+
+		$this->assertSame( $first_call, $second_call, 'Cached routes should be identical to the initial result.' );
+	}
+
 	/**
 	 * @ticket 50244
 	 */
