diff --git a/modules/multiplayer/scene_multiplayer.h b/modules/multiplayer/scene_multiplayer.h
index da8c7621341..4482495899f 100644
--- a/modules/multiplayer/scene_multiplayer.h
+++ b/modules/multiplayer/scene_multiplayer.h
@@ -171,6 +171,7 @@ public:
 	bool is_server_relay_enabled() const;
 
 	Ref<SceneCacheInterface> get_path_cache() { return cache; }
+	Ref<SceneReplicationInterface> get_replicator() { return replicator; }
 
 	SceneMultiplayer();
 	~SceneMultiplayer();
diff --git a/modules/multiplayer/scene_replication_interface.cpp b/modules/multiplayer/scene_replication_interface.cpp
index 9302ea4f20c..7d9437936aa 100644
--- a/modules/multiplayer/scene_replication_interface.cpp
+++ b/modules/multiplayer/scene_replication_interface.cpp
@@ -257,15 +257,54 @@ void SceneReplicationInterface::_visibility_changed(int p_peer, ObjectID p_sid)
 	Node *node = sync->get_root_node();
 	ERR_FAIL_COND(!node); // Bug.
 	const ObjectID oid = node->get_instance_id();
-	if (spawned_nodes.has(oid)) {
+	if (spawned_nodes.has(oid) && p_peer != multiplayer->get_unique_id()) {
 		_update_spawn_visibility(p_peer, oid);
 	}
 	_update_sync_visibility(p_peer, sync);
 }
 
+bool SceneReplicationInterface::is_rpc_visible(const ObjectID &p_oid, int p_peer) const {
+	if (!tracked_nodes.has(p_oid)) {
+		return true; // Untracked nodes are always visible to RPCs.
+	}
+	ERR_FAIL_COND_V(p_peer < 0, false);
+	const TrackedNode &tnode = tracked_nodes[p_oid];
+	if (tnode.synchronizers.is_empty()) {
+		return true; // No synchronizers means no visibility restrictions.
+	}
+	if (tnode.remote_peer && uint32_t(p_peer) == tnode.remote_peer) {
+		return true; // RPCs on spawned nodes are always visible to spawner.
+	} else if (spawned_nodes.has(p_oid)) {
+		// It's a spwaned node we control, this can be fast
+		if (p_peer) {
+			return peers_info.has(p_peer) && peers_info[p_peer].spawn_nodes.has(p_oid);
+		} else {
+			for (const KeyValue<int, PeerInfo> &E : peers_info) {
+				if (!E.value.spawn_nodes.has(p_oid)) {
+					return false; // Not public.
+				}
+			}
+			return true; // All peers have this node.
+		}
+	} else {
+		// Cycle object synchronizers to check visibility.
+		for (const ObjectID &sid : tnode.synchronizers) {
+			MultiplayerSynchronizer *sync = get_id_as<MultiplayerSynchronizer>(sid);
+			ERR_CONTINUE(!sync);
+			// RPC visibility is composed using OR when multiple synchronizers are present.
+			// Note that we don't really care about authority here which may lead to unexpected
+			// results when using multiple synchronizers to control the same node.
+			if (sync->is_visible_to(p_peer)) {
+				return true;
+			}
+		}
+		return false; // Not visible.
+	}
+}
+
 Error SceneReplicationInterface::_update_sync_visibility(int p_peer, MultiplayerSynchronizer *p_sync) {
 	ERR_FAIL_COND_V(!p_sync, ERR_BUG);
-	if (!multiplayer->has_multiplayer_peer() || !p_sync->is_multiplayer_authority()) {
+	if (!multiplayer->has_multiplayer_peer() || !p_sync->is_multiplayer_authority() || p_peer == multiplayer->get_unique_id()) {
 		return OK;
 	}
 
diff --git a/modules/multiplayer/scene_replication_interface.h b/modules/multiplayer/scene_replication_interface.h
index ad68425d774..30d58f71292 100644
--- a/modules/multiplayer/scene_replication_interface.h
+++ b/modules/multiplayer/scene_replication_interface.h
@@ -125,6 +125,8 @@ public:
 	Error on_despawn_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
 	Error on_sync_receive(int p_from, const uint8_t *p_buffer, int p_buffer_len);
 
+	bool is_rpc_visible(const ObjectID &p_oid, int p_peer) const;
+
 	SceneReplicationInterface(SceneMultiplayer *p_multiplayer) {
 		multiplayer = p_multiplayer;
 	}
diff --git a/modules/multiplayer/scene_rpc_interface.cpp b/modules/multiplayer/scene_rpc_interface.cpp
index bfe54c00ff6..dbf2b3751e9 100644
--- a/modules/multiplayer/scene_rpc_interface.cpp
+++ b/modules/multiplayer/scene_rpc_interface.cpp
@@ -295,7 +295,7 @@ void SceneRPCInterface::_process_rpc(Node *p_node, const uint16_t p_rpc_method_i
 	}
 }
 
-void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount) {
+void SceneRPCInterface::_send_rpc(Node *p_node, int p_to, uint16_t p_rpc_id, const RPCConfig &p_config, const StringName &p_name, const Variant **p_arg, int p_argcount) {
 	Ref<MultiplayerPeer> peer = multiplayer->get_multiplayer_peer();
 	ERR_FAIL_COND_MSG(peer.is_null(), "Attempt to call RPC without active multiplayer peer.");
 
@@ -311,12 +311,35 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 		ERR_FAIL_MSG("Attempt to call RPC with unknown peer ID: " + itos(p_to) + ".");
 	}
 
-	// See if all peers have cached path (if so, call can be fast).
-	int psc_id;
-	const bool has_all_peers = multiplayer->get_path_cache()->send_object_cache(p_from, p_to, psc_id);
+	// See if all peers have cached path (if so, call can be fast) while building the RPC target list.
+	HashSet<int> targets;
+	Ref<SceneCacheInterface> cache = multiplayer->get_path_cache();
+	int psc_id = -1;
+	bool has_all_peers = true;
+	const ObjectID oid = p_node->get_instance_id();
+	if (p_to > 0) {
+		ERR_FAIL_COND_MSG(!multiplayer->get_replicator()->is_rpc_visible(oid, p_to), "Attempt to call an RPC to a peer that cannot see this node. Peer ID: " + itos(p_to));
+		targets.insert(p_to);
+		has_all_peers = cache->send_object_cache(p_node, p_to, psc_id);
+	} else {
+		bool restricted = !multiplayer->get_replicator()->is_rpc_visible(oid, 0);
+		for (const int &P : multiplayer->get_connected_peers()) {
+			if (p_to < 0 && P == -p_to) {
+				continue; // Excluded peer.
+			}
+			if (restricted && !multiplayer->get_replicator()->is_rpc_visible(oid, P)) {
+				continue; // Not visible to this peer.
+			}
+			targets.insert(P);
+			bool has_peer = cache->send_object_cache(p_node, P, psc_id);
+			has_all_peers = has_all_peers && has_peer;
+		}
+	}
+	if (targets.is_empty()) {
+		return; // No one in sight.
+	}
 
 	// Create base packet, lots of hardcode because it must be tight.
-
 	int ofs = 0;
 
 #define MAKE_ROOM(m_amount)             \
@@ -399,7 +422,7 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 	ERR_FAIL_COND(name_id_compression > 1);
 
 #ifdef DEBUG_ENABLED
-	_profile_node_data("rpc_out", p_from->get_instance_id(), ofs);
+	_profile_node_data("rpc_out", p_node->get_instance_id(), ofs);
 #endif
 
 	// We can now set the meta
@@ -410,8 +433,9 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 	peer->set_transfer_mode(p_config.transfer_mode);
 
 	if (has_all_peers) {
-		// They all have verified paths, so send fast.
-		multiplayer->send_command(p_to, packet_cache.ptr(), ofs);
+		for (const int P : targets) {
+			multiplayer->send_command(P, packet_cache.ptr(), ofs);
+		}
 	} else {
 		// Unreachable because the node ID is never compressed if the peers doesn't know it.
 		CRASH_COND(node_id_compression != NETWORK_NODE_ID_COMPRESSION_32);
@@ -419,23 +443,15 @@ void SceneRPCInterface::_send_rpc(Node *p_from, int p_to, uint16_t p_rpc_id, con
 		// Not all verified path, so send one by one.
 
 		// Append path at the end, since we will need it for some packets.
-		NodePath from_path = multiplayer->get_root_path().rel_path_to(p_from->get_path());
+		NodePath from_path = multiplayer->get_root_path().rel_path_to(p_node->get_path());
 		CharString pname = String(from_path).utf8();
 		int path_len = encode_cstring(pname.get_data(), nullptr);
 		MAKE_ROOM(ofs + path_len);
 		encode_cstring(pname.get_data(), &(packet_cache.write[ofs]));
 
-		for (const int &P : multiplayer->get_connected_peers()) {
-			if (p_to < 0 && P == -p_to) {
-				continue; // Continue, excluded.
-			}
-
-			if (p_to > 0 && P != p_to) {
-				continue; // Continue, not for this peer.
-			}
-
+		// Not all verified path, so check which needs the longer packet.
+		for (const int P : targets) {
 			bool confirmed = multiplayer->get_path_cache()->is_cache_confirmed(from_path, P);
-
 			if (confirmed) {
 				// This one confirmed path, so use id.
 				encode_uint32(psc_id, &(packet_cache.write[1]));