RunService.Heartbeat is not safe to access in parallel?

Hey, y’all.

Was experimenting with parallelism to optimize some things in my game.
I have the following structure:
imagem

In the Bananator script (please do not take these names seriously), I was trying to access the following function in ProjectileHandler:

function heartbeatConnection(bullet: Projectile): RBXScriptConnection
	return RunService.Heartbeat:ConnectParallel(function(deltaTime)
		local instant = workspace:GetServerTimeNow()
		for i = bullet._lastThrotle, instant - THROTLE, THROTLE do
			local newPosition = throttle(bullet, THROTLE)
			bullet._lastThrotle = i
			
			if not bullet.instance then continue end
			assert(bullet.instance.PrimaryPart, "Bullet "..bullet._species.." does not have PrimaryPart!")
			bullet.instance.PrimaryPart.Position = newPosition
		end
	end)
end

With this said, I constantly have the error “Event RunService.Heartbeat is not safe to access in parallel”, although I found no evidence that Heartbeat was unsafe in parallel, within the documentation.

I also tried creating a connection in parallel with this same event in the Bananator script, and that worked, without throwing that same error.

Is there a way to fix this? Thank you, in advance.

1 Like

Not all Roblox services, especially shared resources like RunService.Heartbeat, are thread-safe or designed for parallel execution.
If you want to handle projectile updates with parallelism, you can’t use RunService.Heartbeat directly in parallel threads. You can try to delegate computations to parallel functions that don’t directly manipulate the Heartbeat event. Move all Roblox-specific API calls (like updating positions or accessing instances) to the sequential section.

Example Solution

-- Main sequential thread that handles updates
function heartbeatConnection(bullet: Projectile): RBXScriptConnection
    return RunService.Heartbeat:Connect(function(deltaTime)
        -- Do the parallel computation first
        task.synchronize(function()
            local instant = workspace:GetServerTimeNow()
            -- Your parallel task (without direct Heartbeat calls)
            task.desynchronize()
            
            for i = bullet._lastThrottle, instant - THROTTLE, THROTTLE do
                local newPosition = throttle(bullet, THROTTLE)
                bullet._lastThrottle = i

                -- Back to synchronized part for instance manipulations
                task.synchronize()
                if not bullet.instance then continue end
                assert(bullet.instance.PrimaryPart, "Bullet "..bullet._species.." does not exist!")
                bullet.instance.PrimaryPart.Position = newPosition
            end
        end)
    end)
end

Documentation on this: Parallel Luau | Documentation - Roblox Creator Hub

To be clear; RunService.Heartbeat:ConnectParallel() is doable but what matters is the type of operations that happen within the parallelized function.

  • Parallel-safe tasks: Operations such as mathematical calculations or processing data can be done in parallel.
  • Not parallel-safe tasks: Anything that modifies game objects (such as setting the position of a Part or calling functions on instances) needs to be synchronized.

Before modifying anything related to Roblox instances (e.g., positions, properties), you need to call task.synchronize() to bring the code back to the main thread, as modifying instances in parallel will result in thread safety violations.

I hope this cleared it up for you :slight_smile:

you cannot connect to events during parallel
you can only call Connect or ConnectParallel in serial execution

so, do this:

function heartbeatConnection(bullet: Projectile): RBXScriptConnection
	task.synchronize() -- return to serial execution before connecting
	return RunService.Heartbeat:ConnectParallel(function(deltaTime)
		local instant = workspace:GetServerTimeNow()
		task.synchronize() -- writing to instance properties are not allowed in parallel, so return to serial here as well
		for i = bullet._lastThrotle, instant - THROTLE, THROTLE do
			local newPosition = throttle(bullet, THROTLE)
			bullet._lastThrotle = i
			
			if not bullet.instance then continue end
			assert(bullet.instance.PrimaryPart, "Bullet "..bullet._species.." does not have PrimaryPart!")
			bullet.instance.PrimaryPart.Position = newPosition
		end
	end)
end

btw, if the documentation doesn’t give a tag about thread safety for something, it means that it’s unsafe (cannot use in parallel)
if it says Read Parallel, you can only edit the property/call the function in serial
if it says Write Parallel, you can absolutely use it in parallel

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.