RemoteEvent may reach a client faster than it takes for a RemoteFunction to travel back from the server to a client

When a client calls a RemoteFunction to fetch data from the server and the server sends a RemoteEvent to the client at around the same time that the RemoteFunction has reached the server, there is a chance that the server’s RemoteEvent signal will reach the client before the RemoteFunction has returned to the client. This sounds a bit vague so to better illustrate the problem, consider the following example.

Example

When a player joins a game, they may want to ask the server if they can join an ongoing round. The server will have a boolean stored in a script indicating if players are allowed to join the ongoing round. When a player joins, they invoke a RemoteFunction to grab and return this value. When this value changes on the server, the server calls a RemoteEvent for all players with the new value of the boolean. In this example, the player should always have the correct value of the boolean observed on the client.

When the value of the boolean changes on the server while the RemoteFunction is ‘on its way’, one of two cases should happen:

Case 1

  1. The client calls the RemoteFunction
  2. The RemoteFunction reaches the server and returns the value ‘false’
  3. Before the RemoteFunction has returned to the client, the value on the server changes from ‘false’ to ‘true’
  4. Right after the value changes, the RemoteEvent gets called and sends the value ‘true’ to the client
  5. The RemoteFunction arrives back at the client and the client sets their stored boolean to ‘false’.
  6. The RemoteEvent reaches the client with the value ‘true’ and the client now changes their value from ‘false’ to ‘true’.

Case 2

  1. The client calls the RemoteFunction
  2. Before the client reaches the server, the boolean on the server changes from ‘false’ to ‘true’
  3. Right after the value changes, the RemoteEvent gets called and sends the value ‘true’ to the client
  4. The RemoteFunction reaches the server and returns the value ‘true’
  5. The RemoteEvent reaches the client with the value ‘true’ and the client now sets their value to ‘true’
  6. The RemoteFunction arrives back at the client and the client sets their stored boolean to ‘true’, even though it was already set to ‘true’.

However, there is a small chance that case 3 happens, which may cause bugs in your game.

Case 3

  1. The client calls the RemoteFunction
  2. The RemoteFunction reaches the server and returns the value ‘false’
  3. Before the RemoteFunction has returned to the client, the value on the server changes from ‘false’ to ‘true’
  4. Right after the value changes, the RemoteEvent gets called and sends the value ‘true’ to the client
  5. The RemoteEvent reaches the client with the value ‘true’ and the client sets their local value to ‘true’
  6. The RemoteFunction arrives back at the client and the client changes their stored boolean from ‘true’ to ‘false’.

In this third case, the boolean has incorrectly changed to false on the client! RemoteFunctions and RemoteEvents are supposed to arrive in the right order, however in this case they don’t (assuming there are no yields, which there aren’t any in the given example). The RemoteEvent is fired after the RemoteFunction is on its way back to the client but arrives before the RemoteFunction has arrived back!

My friend @GPTelos was kind enough to provide this reproduction file below which shows the problem. Sometimes when you run the file in play solo, the LocalScript will output that stateClient.SpawningEnabled is set to ‘false’. This should in theory never happen, but it does. It should never happen because there are no yields, the RemoteEvent is fired immediately after the boolean changes on the server, and the RemoteFunction immediately returns the boolean from the server.

Late_RemoteFunction_Repo.rbxl (56.4 KB)

In the reproduction file it may be required to change the for-loop in the server script to wait slightly longer to better line up the requests. In my case I had to change it to loop 20 times. After that, when running the file I sometimes output ‘true’ as the final value and sometimes ‘false’.

In the given file the output should always return ‘true’ however.

6 Likes

I’m a little confused here. Wouldn’t case 3 be expected behavior since you are firing the remote event before you return in the remote function?

Either way this honestly just sounds like a race condition and if you have to worry about which gets to the client first you may need to redesign your system…

1 Like

I’m a little confused here. Wouldn’t case 3 be expected behavior since you are firing the remote event before you return in the remote function?

No. If the RemoteEvent is fired before the return takes place, the value of the boolean will have been set to ‘true’ already, meaning the value returned by the RemoteFunction should have been ‘true’, but in case 3 the returned value is ‘false’. Roblox also guarantees that packets arrive in order, so if a RemoteEvent A is fired to the client before a RemoteEvent B, then RemoteEvent A will always be executed on the client first. The observed bug breaks this guarantee for a RemoteFunction + RemoteEvent pair.

The code pattern I described is designed to always handle race conditions properly given the supposed guarantee in the packet ordering, but it currently doesn’t due to the bugged behavior. I understand you mean well with your advice but this is bug-reports, not scripting-support. If I needed code advice I would have posted there. :stuck_out_tongue:

I see the issue that you are detailing now. Only cause I could see for this is that the remote function returns on the server but before it reaches the client, the remote event code runs on the server and reaches the client first. But this relies on both running at nearly the exact same time which would then make this a race condition. This could be prevented by restructuring some of the logic but like you said that is not the point of this post

1 Like

Hey! Thank you so much for reporting this issue, we are actively looking into it!

There are some questions I’d like to ask for background information that would really help us debug why this is happening:

  1. Is this an issue that started occurring recently, or did the Engine behave like this for a long time?
  2. If this is a recent issue, do you have an idea as to approximately when this started happening? If we can narrow down the timeframe, the fix may be as simple as reverting a recent change to the Engine.
2 Likes

Hey! Thanks for the quick response. I believe I first witnessed this a couple weeks ago, but I am not sure on the exact time-frame. A colleague of mine mentioned encountering a similar problem recently as well. Assuming their issue was related to this bug report, the bug would have been around already on September 16th. I’m not 100% confident in these time-frames though, but I hope they can serve as a good starting point!

2 Likes

We’ve taken a look at this issue, and it occurs because of the way we schedule events and yielding methods in the engine. Specifically, yielding methods will be resumed at a later point in the frame than events resulting in the behavior you are seeing.

This is unintuitive, so we are looking at improvements we can make. However, there are no concrete plans at this time as any changes could have side-effects on other experiences.

In the mean time, you can workaround this by emulating RemoteFunctions with the RemoteEvents that you need ordered on the same timeline. As you’re already subscribing to the RemoteEvent for updates you can have the server issue a one-time invocation to the client when they connect so that they client can receive the initial value.

On the client you would handle the event as you already are, then have the client request the initial value by either triggering the event on the server or some other mechanism of your choosing.

-- Client
local someValue = nil

-- Your existing event handler
someEvent.OnClientEvent:Connect(function (newValue)
    someValue = newValue
end)

someEvent:FireServer() -- request initial value

Then on the server you can immediately fire the event back with the initial value (the same as you would for an update).

-- Server
someEvent.OnServerEvent:Connect(function (player)
    someEvent:FireClient(player, currentValue)
end)

If you need to block on the client for the initial value to be received you could also use Wait or Once in your client script.

someEvent:FireServer() -- request initial value
someValue = someEvent.OnClientEvent:Wait() -- wait for receive
5 Likes

UPDATE: A previous version of this reply had said that if you use Wait or Once on your client-side RemoteEvent handling LocalScript, you should get the ordering you desire between your code that follows waiting for the RemoteEvent and RemoteFunction:InvokeServer. It turns out, this is not true. There is not an execution ordering guarantee between LocalScripts that were waiting on RemoteEvents vs. RemoteFunctions.

I wish I had a nice pattern I could show you to generically keep consistent ordering between RemoteEvents and RemoteFunctions, but none exist - you need to write manual, context aware code. Giving you a better way to write this type of code is something we’re now looking into, but again, it’s sadly not something that’s easy for us to fix.

3 Likes

Just for clarification, is the event-scheduling behavior you mention here the same for client-server events as it is for client-sided events? In essence, can the behavior from my bug report also be witnessed when dealing with BindableEvent and BindableFunction instances?

I wanted to ask this question because I believe I ran into a similar issue today but with Bindable instances. If the same behavior can be witnessed with BindableEvents/BindableFunctions then today’s issue is probably the same one as I reported here initially. If not, then I may need to double check if I didn’t run into another engine bug.


And while I’m here I may as well ask, is there any chance a fix for this problem could be bundled with the ‘deferred events’ feature once that is finalized, given that deferred events may also cause side-effects in some specific code patterns? No worries if that last question cannot be answered. :smile:

2 Likes

Regarding your first question, yes, you can expect to see the same unintuitive behavior with BindableEvents/BindableFunctions as well.

Regarding your second question, I have forwarded it to the Engineer with expertise in that area. They need some time to think about it, it’s a tough subject like you noticed :slight_smile:

2 Likes