RunService.Heartbeat not running on the server, but only sometimes

What do you want to achieve?

I’m trying to make a candy tool that activates a random candy effect. It’ll get a random ID from a list of candy effects, and do something to the player character or in the game once activated.

All of this works but none of the RunService.Heartbeat events, or as I call them loops, within the function ever run.

What is the issue?

Connections to events within the candy handler are not firing at all, but only when the function is ran inside of a tool. When ran using my custom admin command system, the events run perfectly fine (but I obviously want them to run in a tool.)

The Candy Handler function:

function CandyEffects:HandleEffectAsync(effectId:string, player:Player, character:Model, humanoid:Humanoid)
	print("HandleEffectAsync", effectId, player, character, humanoid)
	if effectId == "Messy" then
		if not character then return end
		if not character.Parent then return end
		local root = character.PrimaryPart
		if not root then return end
		
		local balls = {}
		local amount = math.random(13, 24)
		for _ = 1, amount do
			local ball:Part = ReplicatedStorage.Objects.Modifiers.BouncyBall:Clone()
			ball.Position = root.Position + Vector3.new(math.random(-5, 5), math.random(-5, 5), math.random(-5, 5))
			ball.Size = Vector3.new(1,1,1) * math.random(2, 5)
			ball.CanQuery = false
			ball.CollisionGroup = "Debris"
			ball.Parent = workspace.Debris
			table.insert(balls, ball)
		end

		RunService.Heartbeat:Wait()
		for _, ball in ipairs(balls) do
			ball.AssemblyLinearVelocity += Vector3.new(math.random(-250, 250), math.random(-250, 250), math.random(-250, 250))
		end
	-- ...
	elseif effectId == "RazorBlades" then
		print("rz1", character, humanoid)
		if not character then return end
		if not humanoid then return end
		print("rz2", character, humanoid)
		
		local connection:RBXScriptConnection
		local canTakeDamage = true
		local hitTimes = 0
		print("made connection")
		connection = RunService.Heartbeat:Connect(function()
			print("loop")
			if not canTakeDamage then return print("cannot take dmg") end
			if hitTimes >= 16 then print("did 16 hits") return connection:Disconnect() end
			if not character.Parent then print("no character parent") return connection:Disconnect() end
			if not humanoid.Parent then print("no humanoid parent") return connection:Disconnect() end
			if humanoid.Health <= 0 then print("player died") return connection:Disconnect() end

			print("do damage")
			humanoid:TakeDamage(1.3477893139510)
			hitTimes += 1

			canTakeDamage = false
			print("do waiting")
			task.wait(0.5)
			print("stopped waiting")
			canTakeDamage = true
		end)
	elseif effectId == "Train" then
		if not player then return end
		if not player.Parent then return end
		ReplicatedStorage.Events.CandyEffects.Train:FireClient(player)
	-- more effects...
	end
end

I’ve singled out 3 candy effects in particular:
The Messy effect works, but none of the bouncy balls are given velocity
The RazorBlades effect does not work at all, the rz1, rz2 and made connection log will all be made but none of the code inside the Heartbeat event runs
and The Train effect works perfectly (the event is fired to the client properly)

The tool code:

local ServerScriptService = game:GetService("ServerScriptService")

local CandyEffects = require(ServerScriptService.CandyEffects)
local CandyTool = require(ServerScriptService.ScriptedTools.CandyTool)

local Tool = script.Parent
local Handle = Tool.Candy

local CandyVariant = Tool:GetAttribute("CandyVariation") or "CandyCircleRed"
CandyTool:CreateTool(Tool, Handle, NumberRange.new(0), CandyVariant)

The CandyTool module:

-- Acts like a class to be inherited, since all of the candy tools are exactly the same.
function CandyTool:CreateTool(tool:Tool, handle:BasePart, levelNumberRange:NumberRange, saveId:string?)
	local Tool = tool
	local Handle = handle
	
	local OwnerPlayer:Player = nil
	local OwnerCharacter:Model = nil
	local OwnerHumanoid:Humanoid = nil

	local Equipped = false
	local CanEat = true

	local animationMotor:Motor6D = nil
	local animationH:AnimationTrack = nil
	local animationU:AnimationTrack = nil
	
	-- if the tool wasnt already initialized with a variation, do it with the provided one here
	if saveId then
		Tool:SetAttribute("CandyVariation", saveId)
	end
	
	Tool.Equipped:Connect(function()
		local character = Tool:FindFirstAncestorWhichIsA("Model")
		if not character then return end
		local player = PlayersService:GetPlayerFromCharacter(character)
		if not player then return end
		local humanoid = character:FindFirstChildWhichIsA("Humanoid")
		if not humanoid then return end
		local rightHand = character:FindFirstChild("RightHand")
		if not rightHand then return end

		Equipped = true
		OwnerPlayer = player
		OwnerCharacter = character
		OwnerHumanoid = humanoid
		
		-- this just makes it so it runs the KnownCandyEffect if the same tool is used again
		if saveId and module.SavedEffects[saveId] then
			Tool:SetAttribute("KnownCandyEffect", module.SavedEffects[saveId])
		end

		local animator = humanoid:FindFirstChildWhichIsA("Animator")
		if not animator then return end
		
		local motor = Instance.new("Motor6D")
		motor.Part0 = rightHand
		motor.Part1 = Handle
		motor.Name = "CandyMotor"
		motor.Parent = rightHand
		animationMotor = motor
		
		if not (animationH or animationU) then
			animationH = animator:LoadAnimation(Tool.CandyHold)
			animationU = animator:LoadAnimation(Tool.CandyEat)
		end
		animationH:Play()

		Handle.Equip.SoundGroup = workspace.ScriptResources
		Handle.Equip.TimePosition = 0
		Handle.Equip:Play()
	end)
	Tool.Unequipped:Connect(function()
		Equipped = false
		if animationMotor then animationMotor:Destroy() end
		if animationH then animationH:Stop() end
		if animationU then animationU:Stop() end
	end)
	Tool.Activated:Connect(function()
		if not CanEat then return end
		if not Equipped then return end
		CanEat = false
		
		if animationH then animationH:Stop() end
		if animationU then animationU:Play() end
		
		Handle.Eat.SoundGroup = workspace.ScriptResources
		Handle.Eat.TimePosition = 0.05
		Handle.Eat:Play()
		task.wait(0.5)

		if not Equipped then
			CanEat = true
			return
		end

		-- to test this easier i've put RazorBlades by default
		local knownEffectId = module.SavedEffects[saveId] or "RazorBlades"
		if knownEffectId then
			print("Tool", OwnerPlayer, OwnerPlayer.Parent, OwnerCharacter, OwnerCharacter.Parent, OwnerHumanoid, OwnerHumanoid.Parent)
			task.spawn(function()
				CandyEffects:HandleEffectAsync(knownEffectId, OwnerPlayer, OwnerPlayer.Character, OwnerHumanoid)
			end)
			CandyEffects:DisplayEffect(knownEffectId, OwnerPlayer)
		else
			local effect = CandyEffects:HandleAndDisplayRandomEffect(OwnerPlayer, OwnerCharacter, OwnerHumanoid, levelNumberRange)
			if saveId then
				module.SavedEffects[saveId] = effect.id
			end
		end
		
		if animationMotor then animationMotor:Destroy() end
		if animationH then animationH:Stop() end
		if animationU then animationU:Stop() end
		Tool:Destroy()
	end)
end

And the admin command, where all effects work fully with no problems:

function Command:Invoke(initiator:Player, args:{string})
	local targetCandy = tostring(args[1] or "")
	local candyEffect = CandyEffects:GetEffectById(targetCandy, true)
	if not candyEffect then
		ReplicatedStorage.Events.AdminCommands.SendMessage:FireClient(initiator, "That candy effect does not exist. Try removing any symbols or spaces.", "warn")
		return
	end
	
	local targetPlayers = {initiator}
	if args[2] then
		targetPlayers = ArgumentParser:ParsePlayerKeyword(initiator, args[2])
	end
	if #targetPlayers <= 0 then return end
	
	RuntimeManipulator:RevokeGameRewards(initiator)
	for _, player in ipairs(targetPlayers) do
		local humanoid:Humanoid = nil
		if player.Character then
			humanoid = player.Character:FindFirstChildWhichIsA("Humanoid")
		end

		task.spawn(function()
			CandyEffects:HandleEffectAsync(candyEffect.id, player, player.Character, humanoid)
		end)
		ReplicatedStorage.Events.AdminCommands.SendMessage:FireClient(initiator, "Applied " .. candyEffect.name .. " to " .. player.Name, "success")
	end
end

Solutions I’ve tried

I’ve tried adding logs in every step before the HandleEffectAsync function is ran, but from the very beginning and the very end, the player, character, humanoid, and their parents are all valid.
I’m not really sure what other solutions I can even try.