Choosing Between Photon RPCs and RaiseEvent for Realtime Multiplayer
Photon Realtime offers two message primitives—RPCs and RaiseEvent—with distinct reliability, routing, and serialization behaviors. This blog walks through choosing the right one for position updates vs game events, includes a worked code example, and highlights the custom-type serialization trap.
02 Jun 2026, 19:01 UTC

The Core Dilemma: RPC vs RaiseEvent
When building with Photon Realtime, every network message starts with a choice: PhotonView.RPC or RaiseEvent. Both deliver data across clients, but they encode different assumptions about reliability, routing, and serialization. Picking the wrong one for a given message type leads to either wasted bandwidth or subtle desynchronization bugs that surface only under load.
Delivery Guarantees and Latency Trade-offs
RPCs default to reliable delivery: the sender retransmits until the receiver acknowledges. This adds a round-trip for each lost packet, which is fine for infrequent, state-changing calls like "PlayerScored" or "OpenDoor". RaiseEvent lets you choose per call: SendOptions.SendReliable or SendOptions.SendUnreliable. For high-frequency updates—player position, rotation, animation state—unreliable RaiseEvent keeps frame rates stable because dropped packets are simply superseded by the next update.
Photon's reliable channel also sequences messages. If you send a reliable RPC followed by an unreliable RaiseEvent, the unreliable message may arrive first. Design your protocol so ordering dependencies don't cross reliability boundaries.
Routing: Targeted vs Broadcast
RPCs are always routed through a PhotonView attached to a networked GameObject. The receiver set is implicit: all clients with that view, or a specific RpcTarget enum (All, Others, MasterClient, etc.). RaiseEvent decouples routing from GameObjects. You specify ReceiverGroup (All, Others, MasterClient) or an explicit array of int actor numbers. You can also use InterestGroup to limit dissemination to clients subscribed to that group—essential for large worlds where clients only need updates for nearby entities.
Worked Example: Position Sync vs Ability Activation
Consider a 20-player arena shooter. Each player broadcasts position 30 times per second. An ability activation (dash, shield) fires once every few seconds and must be processed by all clients in deterministic order.
// Position update: high frequency, loss tolerant
void SendPosition(Vector3 pos, Quaternion rot) {
var opts = new SendOptions { DeliveryMode = DeliveryMode.Unreliable };
PhotonNetwork.RaiseEvent(
EventCodes.PlayerPosition,
new object[] { PhotonNetwork.LocalPlayer.ActorNumber, pos, rot },
new RaiseEventOptions { Receivers = ReceiverGroup.Others },
opts
);
}
// Ability activation: low frequency, must arrive in order
[PunRPC]
void ActivateAbility(int abilityId, Vector3 target) {
// deterministic logic here
}
public void RequestAbility(int abilityId, Vector3 target) {
photonView.RPC(nameof(ActivateAbility), RpcTarget.AllViaServer, abilityId, target);
}
The position update uses unreliable RaiseEvent with ReceiverGroup.Others to avoid echoing back to sender. The ability uses an RPC with RpcTarget.AllViaServer so the server orders the call relative to other reliable messages (e.g., damage resolution). Both paths are logged in Photon's client debug output as RaiseEvent sent and RPC invoked with sender/receiver actor IDs—verify routing by filtering those log lines during a two-client test.
Custom Types: Serialization Pitfalls
Both RPCs and RaiseEvent serialize parameters via Photon's protocol. Primitive types and Unity vectors work out of the box. For custom structs or classes, you have two paths:
- Implement
ISerializableand register withPhotonPeer.RegisterTypeon startup (client and server plugin if using Enterprise Cloud). - Use Photon's generated serialization (IL2CPP/Unity 2021.3+) which emits serializers for types marked
[Serializable]and containing only supported fields.
A version mismatch—adding a field on the client but not the server plugin—causes silent payload corruption or disconnects with generic SerializationException. Always run a unit test that spawns two clients, sends the custom type through both RPC and RaiseEvent, and asserts the receiver's OnEvent or OnPhotonSerializeView callback reconstructs the exact values.
Limitation: No Built-in Fragmentation for Large Payloads
Photon's MTU is ~1200 bytes. Neither RPC nor RaiseEvent fragments larger messages automatically. If you need to send a 5 KB level chunk, you must chunk it yourself, sequence the parts, and reassemble on the receiver. This is a common surprise when migrating from Photon Bolt or Mirror, which handle fragmentation transparently.
Actionable Checklist
- Classify each message: state-change (reliable RPC) vs high-frequency update (unreliable RaiseEvent).
- Use
InterestGroupfor RaiseEvent when player count > 10 and updates are spatially local. - Register all custom types in a single static initializer before
PhotonNetwork.ConnectUsingSettings. - Add a debug-only script that logs
PhotonNetwork.NetworkingClient.LoadBalancingPeer.DebugOutatExitGames.Client.Photon.DebugLevel.ALLand filters for "RPC invoked" / "RaiseEvent sent" during integration tests. - If payloads exceed 1 KB, implement application-layer fragmentation with sequence IDs and a reassembly buffer.
Start with the classification step—it's the single decision that prevents most latency and desync issues downstream.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.