ComputeAsync Pathfinding Starts Lagging After Creating Enough Paths

I have used a tutorial pathfinding script as a base, then built on top of it for the functionality I need. An issue I found very early is that after chasing the player for long enough (after creating ~150 ComputeAsync paths) the ComputeAsync becomes very obviously laggy and stutters when trying to create a path., eventually creating only one waypoint over dozens of seconds. I have narrowed this down to the ComputeAsync function as the rest of the script runs fine and there’s nothing else in the game to create a memory leak elsewhere. It is very frustrating because the pathfinding works perfect at first, then seemingly deteriorates after running for a while.

None of the solutions I have come up with or looked up online have worked. I have tried refreshing every variable every path creation, I have tried refreshing every variable on a timer, I have tried giving network ownership of the NPC to the player, I have tried copy and pasting a completely different person’s pathfinding script (that also used ComputeAsync), disabling and reenabling the script also did nothing, and, my absolute last resort, deleting the pathfinding script and duplicating an exact copy back into the NPC also did not help.

I will clarify just to be absolutely safe. I do not need someone to create a pathfinding script for me; that is already done. The pathfinding is not “failing” (blocked by obstacles, inaccessible platform, etc.), it is just taking forever to create the waypoints. I have already tried flushing the variables to be brand new in multiple ways, if this is the issue then it must be some hidden method that somehow eluded me this entire time.

function Pathfind()
	
	while SearchAttempts <= 10 do
		local waypoints = nil
		target = nil
		path = nil
		task.wait(0.25)
		path = PathfindingService:CreatePath({
			AgentCanJump = false,
			AgentCanClimb = false,
			Costs = {
				Neon = 1000,
				Plastic = 1,
				DangerZone = 10000,
			},
			AgentRadius = 2.7,
			AgentHeight = 2,
		})
		GetClosestPlayer()
		
		if GetClosestPlayer() ~= nil then
			npc:SetAttribute("Target",GetClosestPlayer().Name)
			npc:SetAttribute("HasTarget",true)
			
			local GoalPlayer = GetClosestPlayer()
			
			if GoalPlayer ~= nil then
				target = game.Workspace.Players[npc:GetAttribute("Target")].HumanoidRootPart.Position
			else
				
			end

			local succ, err = pcall(function()
				path:ComputeAsync(npc.PrimaryPart.Position,target)
			end)

			if npc:GetAttribute("SeesTarget") == true and succ and path.Status == Enum.PathStatus.Success then
				npc:SetAttribute("State","Chasing")
				npc:SetAttribute("HasTarget",true)
				--print("NPC has a target.")
			else
				--warn(err)
			end
			
			if npc:GetAttribute("State") == "Chasing" then
				if npc.FollowPlayer.Enabled == false then
					npc.FollowPlayer.Enabled = true
				end
			elseif npc:GetAttribute("State") == "Searching" then
				if npc.FollowPlayer.Enabled == true then
					npc.FollowPlayer.Enabled = false
				end
				
				for i, waypoint in pairs(path:GetWaypoints()) do
					
					local part = Instance.new("Part")
					part.Material = "Neon"
					part.Anchored = true
					part.CanCollide = false
					part.Shape = "Ball"
					part.Position = waypoint.Position

					part.Parent = game.Workspace.whoa
					hum:MoveTo(waypoint.Position)
					game.Debris:AddItem(part,1)

					hum.MoveToFinished:Wait()
				end
				
				path:Destroy()
				
				--print("End of path.")
				if npc:GetAttribute("SeesTarget") == false then
					if npc.FollowPlayer.Enabled == true then
						npc.FollowPlayer.Enabled = false
					end
					npc:SetAttribute("State","Node")
				end
				
				if npc:GetAttribute("State") == "Node" then
					target = OGSpot

					local succ, err = pcall(function()
						path:ComputeAsync(npc.PrimaryPart.Position,target)
					end)
					
					for _, waypoint in pairs(path:GetWaypoints()) do
						local part = Instance.new("Part")
						part.Material = "Neon"
						part.Anchored = true
						part.CanCollide = false
						part.Shape = "Ball"
						part.Color = Color3.new(1, 0, 1)
						part.Position = waypoint.Position

						part.Parent = game.Workspace.whoa
						game.Debris:AddItem(part,1)
						hum:MoveTo(waypoint.Position)

						hum.MoveToFinished:Wait()
					end
					path:Destroy()
					npc:SetAttribute("State","Idle")
				end
			end
		else
			npc:SetAttribute("HasTarget",false)
			npc:SetAttribute("Target",nil)
		end
		--SearchAttempts += 1
	end
end

I am putting my script here incase I am doing something horribly wrong, but I imagine you could replicate my issue by placing 2 nodes and having an NPC path between them for long enough (this was another test I did but with 3 nodes instead of 2, it still eventually broke). It’s not the entire script, but the other functions work as intended regardless of the ComputeAsync breaking or not.

1 Like

i think you should set network ownership of npc to server :SetNetworkOwner() and make the function a local function local function PathFind()

1 Like

If you’re going to run pathfinding that frequently, I would consider making the NPC/character simulation client-based instead of doing all of it on the server.

By that I mean the server shouldnt be constantly creating the NPC character and calling ComputeAsync for every movement update. The server can handle the important authoritative data, like which NPC exists, its target, state, damage checks and validation. But the actual visual NPC character and frequent path/movement computation can be created and handled on the client.

So the flow would be something like:

  • Server tells the client that an NPC exists.
  • Client creates the visible NPC character locally.
  • Client runs the pathfinding / movement updates for that NPC.
  • Server only sends important state changes or validates important gameplay actions.
  • The server avoids calling ComputeAsync constantly for every NPC.

This is especially useful if the NPCs are mostly visual or dont need full server authoritative physics every moment. If the NPC affects gameplay directly, like dealing damage or blocking players, then the server should still validate those important parts. But the expensive visual movement and frequent path recomputation dont always need to be fully server side.

Basically, if you need that many path updates, make the character creation and path computation client-sided and let the server control only the important gameplay state.

1 Like

This is why roblox games like piggy do not use pathfinding very often with their npcs. It’s just a simple moveto loop for the NPC. If the NPC encounters something that would block it’s path then the path is calculated. And even then it’s only one or two paths per second that the NPC creates.

Calculating 150 paths will lead to these problems. You need to calculate less paths call less compute async methods.

2 Likes

This is a known Roblox engine bug — PathfindingService accumulates internal state over many ComputeAsync calls and degrades. path:Destroy() doesn’t fully clean it up.

The workaround is to recreate the PathfindingService path object less frequently and cap reuse, or more reliably, throttle calls and reuse a single path object with a cooldown. But the most effective fix people have found is wrapping ComputeAsync in a self-limiting queue so you never have concurrent or rapid successive calls stacking up.

Looking at your script, the immediate problems are:

1. GetClosestPlayer() is called 3 times per loop — it should be called once and stored.

2. path:Destroy() is called, then path:ComputeAsync() is called on the destroyed path — this is in your "Node" block. You destroy the path mid-loop then try to reuse it.

3. No cooldown between ComputeAsync callstask.wait(0.25) is too aggressive for repeated pathfinding.

The most impactful fix for the degradation specifically: create the path object once outside the loop, not every iteration.

local path = PathfindingService:CreatePath({
    AgentCanJump = false,
    AgentCanClimb = false,
    Costs = { Neon = 1000, Plastic = 1, DangerZone = 10000 },
    AgentRadius = 2.7,
    AgentHeight = 2,
})

while SearchAttempts <= 10 do
    task.wait(0.5) -- give the engine more breathing room
    
    local goalPlayer = GetClosestPlayer()
    -- rest of logic using the same path object, no Destroy() mid-loop
end

Drop the path:Destroy() calls entirely while the loop is running. Only destroy on cleanup when the NPC is done. The engine’s internal pathfinding graph is shared — destroying and recreating paths rapidly is likely what’s causing the accumulation.

If it still degrades after that, the Last option that works for others is adding a task.wait(1) floor between computes and recycling the path object every ~50 uses by destroying it and creating a fresh one outside the hot path.

2 Likes

Thank you for the tips to clean it up! In hindsight some of these mistakes are silly.

Unfortunately, I either didn’t give you the right information or I didn’t understand your solution. I remade the “pathfind to 3 random nodes” test with your advice in mind and it still breaks after pathing for long enough.

local function GeneratePath()
	print("Generating a path.")
	if path ~= nil then
		print("Deleting.")
		path:Destroy()
	end
	
	path = PathfindingService:CreatePath({
		AgentCanJump = false,
		AgentCanClimb = false,
		Costs = {
			Neon = 1000,
			Plastic = 1,
			DangerZone = 10000,
		},
		AgentRadius = 2.7,
		AgentHeight = 2,
	})
	return path
end
GeneratePath()

function Pathfind()
	while task.wait(0.5) do
		local nodes = game.Workspace.Nodes:GetChildren()
		local RandomNode = math.random(1,#nodes)
		local target = nodes[RandomNode].Position
		
		local succ, err = pcall(function()
			path:ComputeAsync(npc.PrimaryPart.Position,target)
		end)
		
		if succ and path.Status == Enum.PathStatus.Success then
			for _, waypoint in pairs(path:GetWaypoints()) do
				hum:MoveTo(waypoint.Position)
				hum.MoveToFinished:Wait()
			end
		end
		
		SearchAttempts += 1
		
		if SearchAttempts >= 30 then
			SearchAttempts = 0
			GeneratePath()
		end
	end
end

task.spawn(Pathfind)

I know you said path:Destroy() doesn’t clear it correctly, but is there another way to do so? This feels like a cleaner way of trying what I’ve already tried.

1 Like

Wouldn’t this issue show up for them too if a match lasted long enough? There must be something about ComputeAsync I’m missing or doing very wrong because I just can’t see another way for an NPC to pathfind without… pathfinding?

You don’t need to call CreatePath() more than once like that, you can reuse the same path object. It’s a bit confusing but a Path object is more like RaycastParams or something but like if RaycastParams could also perform a raycast on its own.

1 Like

Good to know! But I just ran the above code without creating a new path and it still degrades. Was that supposed to fix it or is it just a rule of thumb? If it’s a rule of thumb, how do I clean the previous path objects without generating a new one?

You don’t need to “clean” it unless you change the NPC’s parameters or something like making it bigger or something. When you compute a new path, the old one is deleted.

also it looks like SearchAttempts isn’t incrementing, if you’re calling this function multiple times using an event listener or a coroutine each instance of it will keep running forever

Only the HRP is important for pathfinding/moving them! The rest can stay on the client

1 Like

No I don’t think so, computeasync is just like any other async method which sends a request to roblox internally. Their internal services can’t handle infinite requests so your paths will slow down if you’re continously calculating paths very frequently.

But usually the limits are only imposed per minute or so and then they reset back to default, they shouldn’t annoy you overtime.

The limits of computeasync are not publicly discplayed and I don’t know if roblox actually has a strict limit on them per mintue or something but if you call it too frequently like all other async methods because they call roblox’s internal services they are rate limited.

How often are you calling computeasync??

1 Like

It’s a script for NPCs to chase the player. They have a simple MoveTo script for direct LoS, then ComputeAsync pathfinding is used for going around walls or checking “search” nodes. I want them to follow players around corners, as if checking their last known location, so ComputeAsync is being used every half a second while in chase since I want the player’s recent location.

If it’s that limited internally, are you implying I should only ComputeAsync if ABSOLUTELY necessary? If so, wouldn’t the NPC stutter until the path is calculated, even for just a second? Also, wouldn’t it eventually degrade anyway? My game won’t be a quick in-and-out thing, so the same NPC may be chasing the player for a minute.

When I said this I meant async methods in general like setasync for datastoreservice are rate limited per minute. I don’t know the actual rate limit for computeasync.

Computeasync likely does not have a strict rigid limit like setasync/getasync from datastoreservice. And we don’t know the exact rate limit. but it definitely has a rate limit is what I meant.

I misread your original post, after I saw you mention less than 150 paths calculated. I thought you were calculating paths very frequently whihc is why I mentioned rate limiting. But from what you just told me, yeah no your computeasync calls are likely well under the hidden rate limit whatever it is.

Just a thought, this piece of code may be the problem:

Creating waypoint baseparts are a great way to debug your pathfinding NPCs. but they can cause lag. You’re creating a new basepart for every single waypoint (very frequently I believe). And the more waypoints you have the more baseparts you are creating for the server and then for the server to replicate to all clients.

I feel like it may be that the pathfinding starts to stutter/lag because your script is spending so much compute time creating those baseparts first on the server, then replicating them to clients. before actually calling humanoid moveto. This also may mean that your path isn’t actually stuttering at all but instead its an illusion because the waypoint baseparts you are creating are taking up compute time.

Could you try removing the code that creates those parts and test then?

Or actually better yet, if that isn’t the issue. Use os.clock to debug lines of your script. You can accurately identify and specifically pinpoint which parts of your code are taking up compute time in your script. Which therefore would lead to the stuttering of the paths you’re talking about.

Something like this:

for _, waypoint in pairs(path:GetWaypoints()) do
local Start = os.clock()
						local part = Instance.new("Part")
						part.Material = "Neon"
						part.Anchored = true
						part.CanCollide = false
						part.Shape = "Ball"
						part.Color = Color3.new(1, 0, 1)
						part.Position = waypoint.Position

						part.Parent = game.Workspace.whoa
						game.Debris:AddItem(part,1)
local End = os.clock(0 - Start
print(End) -- See how much time this takes for the script to finish creating the part
						hum:MoveTo(waypoint.Position)

						hum.MoveToFinished:Wait()
					end

I may be wrong about the creating instances lag theory so if that isn’t the case spam os.clock all around your script to see how long it’s taking for parts of your script to compute.

The only time games like piggy use computeasync is when the player is behind a wall. But if you keep going around a table over and over it will continously create paths over and over. Yet there’s usually no jitter. So only use computeasync when you’re behind walls and stuff yk.

Just a thought, this piece of code may be the problem:

You’re right, they’re Parts that spawn in to showcase the pathfinding and possibly debug it, though I have disabled them before and I’ve disabled them now and the pathfinding still breaks. They don’t seem related to the issue.

In fact, I just tried a blank project, literally nothing else except the NPC and the nodes to path towards, and it still breaks. This got me curious, and I realized that the NPC was breaking at call counts that seemed random. There was a moment when I had the NPC moving very fast with a high limit for the SearchAttempts (AKA ComputeAsyncs, as this variable only increases in the loop where it is called), and it broke at ~150, as stated before. I slowed the NPC down to a more realistic speed, and now they’re breaking at ~25 SearchAttempts. I got the idea to time how long it took the pathfinding to break, so I used my phone’s stopwatch to time it, and the pathfinding breaks at ~1 minute of running, regardless of how fast the NPC is moving or how many breaks I give it. Even setting the limit very low (20-30 SearchAttempts before stopping for 2 minutes) still broke the pathfinding when the break was over.

I’m not sure if this information will help, but I tried adding os.clock() to the loop and it was consistently increasing by 1 millisecond (I guess it’s in milliseconds?) every call, starting around 3200, but starts higher with variables that make sense, like the NPC moving faster.
EDIT: It is not in milliseconds as that would imply it’s taking 3+ seconds to calculate the path, which, at least at first, it is not.

The only time games like piggy use computeasync is when the player is behind a wall. But if you keep going around a table over and over it will continously create paths over and over. Yet there’s usually no jitter. So only use computeasync when you’re behind walls and stuff yk.

That’s what’s confusing me. Many games use pathfinding similar to this and I’ve never seen them stutter. I’m starting to think there’s some secret plugin pathfinding they all use instead of the default Roblox one because it’s seemingly only me with this issue lmao

After finding out it breaks after 1 minute and not after a certain amount of calls, I tried resetting the while loop that ComputeAsync is in and that didn’t change anything. I’ve already tried refreshing variables like zurai told me to do and that unfortunately didn’t fix it either.

Yeah this seems like a really weird case, but I don’t think computeasync would lead to pathfinding breaking overtime.

I have an idea but it’s not a proper fix.

you could consistently break the loop after some time and make a brand new one over and over again. This is technically a “fix” since as you said the path detoriates overtime in the loop. If you recreate the loop over and over you have a new set of time that you can use over and over.

Maybe try putting os.clock in specific parts of your code not just top to bottom so you can actually pinpoint where it’s coming from. ComputeAsync shouldn’t really be causing issues like this. Atleast I haven’t encountered these bugs you’re facing.

A game called spider has its spider bot use pathfinding constantly. Not even just behind walls. And their bot is arguably even cleaner than piggy’s. So yeah I don’t believe computeasync is the problem.

I don’t think this is likely but it could be that there is an issue with your NPC itself. Could you show how your NPC is set up in the explorer?

I also heard some rumors that humanoid moveto is buggy, maybe you can try to use lerps/tweening instead and see how that turns out?

How many waypoints are usually calculated in aach path of yours? If there are many waypoints that could be the reason why it’s slowing down the path overtime.. But that’s just my thought.

Maybe we need more code not just the pathfind function but also the rest of your code because it could be something else too.

Once you get rid of computeasync and you test with os.clock, does the script complete the function in a normal speed or?

Also i’m pretty sure every pathfinding module uses computeasync under the hood. You could try and make your own pathfinding with raycasting and such but it might be difficult. I can’t realy say because i’ve never tried making my own pathfinding system without the roblox pathfindingservice.

1 Like

One other Idea I have in mind is to not use pathfinding at all. And instead scatter nodes all around your map (this will only work if your map is consistent and unchanged like piggy). Not only will this make your pathfinding cleaner but you’re likely not going to encounter those bugs anymore because you’re using computeasync.

This will be more difficult than using roblox’s pathfindingservice that automatically does the work for you but the end result might make it worth it. Even piggy’s pathfinding bots look kinda stupid sometimes.

1 Like

I see now os.clock is essentially just printing my system time and I’m supposed to use the gap in prints to see where the delay is coming from. I did not know this and associated the higher number with it taking longer to calculate the path, when in reality it was just getting late lol, that’s on me.

Maybe try putting os.clock in specific parts of your code not just top to bottom so you can actually pinpoint where it’s coming from. ComputeAsync shouldn’t really be causing issues like this.

You’re right again, actually. The ComputeAsync os.clock is printing instantly and it’s the Waypoint MoveTo loop that’s taking its time. It goes from taking 1… whatever this is measured in… to as big of a gap as 20. I’ll try your tween idea and see what happens.

You did ask, so here’s the NPC in the Explorer.

And the old ones are outdated now, so here’s the code in the picture running in Pathfinding.

local PathfindingService = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")
local plrs = game:GetService("Players")

local npc = script.Parent
local hum = npc:WaitForChild("Humanoid")
local rootpart = npc:WaitForChild("HumanoidRootPart")
local SearchAttempts = 0
local GoalPlayer = nil
local waypoint = nil
local waypoints = nil
local goal = nil
local target = nil
local path = nil
local var = false
local OGSpot = rootpart.Position

npc:SetAttribute("State","Idle")
npc:SetAttribute("HasTarget",false)

local range = 80
local Start = os.clock()

local function GeneratePath()
	if path ~= nil then
		print("Deleting.")
		path:Destroy()
		waypoint = nil
		waypoints = nil
		hum = nil
		
		hum = npc.Humanoid
	end

	print("Generating a path.")

	path = nil
	task.wait(1)
	path = PathfindingService:CreatePath({
		AgentCanJump = false,
		AgentCanClimb = false,
		Costs = {
			Neon = 1000,
			Plastic = 1,
			DangerZone = 10000,
		},
		AgentRadius = 2.7,
		AgentHeight = 2,
	})
	return path
end
GeneratePath()

function Pathfind()
	while task.wait(1) do
		local End = os.clock(0 - Start)
		--print(End,"Start of pathfinding loop")
		print(SearchAttempts)
		if SearchAttempts < 35 then
			local nodes = game.Workspace.Nodes:GetChildren() -- You could replace the Nodes folder (not visible on any Forum post) with any positions you wish. The NPC just needs somewhere to go.
			local RandomNode = math.random(1,#nodes) -- The NPC will sometimes randomly roll the node it's already on and not move. I don't consider this a bug and I doubt it has much effect on the pathfinding issue.
			local target = nodes[RandomNode].Position

			local succ, err = pcall(function()
				path:ComputeAsync(npc.PrimaryPart.Position,target)
				local End = os.clock(0 - Start)
				print(End,"ComputeAsync")
			end)

			if succ and path.Status == Enum.PathStatus.Success then
				for _, waypoint in pairs(path:GetWaypoints()) do
					hum:MoveTo(waypoint.Position)
					hum.MoveToFinished:Wait()
				end
				local End = os.clock(0 - Start)
				print(End,"Waypoint MoveTo loop")
			end


			SearchAttempts += 1
		else
			var = true
		end

		if SearchAttempts >= 35 then
			SearchAttempts = 0
			GeneratePath()
		end
	end
end

Pathfind()

RunService.Heartbeat:Connect(function()
if var == true then
		warn("RESETTING WHILE LOOP")
		SearchAttempts = 0
		var = false
		task.wait(5)
		Pathfind()
	end
end)

-- You can change the NPC's speed, the distance of the nodes, etc. etc., to my knowledge nothing will change the outcome
-- the NPC will begin stuttering ~1 minute after the game starts
1 Like

Slight breakthrough.

if succ and path.Status == Enum.PathStatus.Success then
				for _, waypoint in pairs(path:GetWaypoints()) do
					hum:MoveTo(waypoint.Position)
					--hum.MoveToFinished:Wait()
				end
				local End = os.clock(0 - Start)
				print(End,"Waypoint MoveTo loop")
			end

If I comment out the hum.MoveToFinished:Wait() line, the NPC stops stuttering, but now he walks into walls (I assume because he’s trying to MoveTo the last node that was generated, since the debug Parts mentioned earlier still show a correct path) and changes chase states instantly. I definitely need a MoveToFinished in there since it’s pretty critical, but it’s apparently what’s causing the stutter…

I’ve already tried wiping and duplicating the script, refreshing variables (including the humanoid reference), and many other kinds of ways to “clean up” the function. I’m at such a loss for what I could possibly do here.

1 Like

Yes the stuttering I believe is a bug with humanoid move to. Humanoid move to finished has problems for whatever reason. I’ve heard it’s not exactly optimised. It could be because humanoid move to relies on physics.

If you try tweening your npc movement. And then add tween.Completed:Wait(). The stuttering should stop.

By the way, the way os.clock() is intended to be used is firstly without a 0 written in there lol and second you need to place the start variable within the loop. Otherwise when you subtract the end time from the start time it will seem like it’s taking much longer because u saved the start time from when the script just executed. Take a look at my previous example with os.clock.

The waypoint loop may be using 20 seconds but that’s pretty much the point and there’s nothing wrong with that. As you’re stopping the loop from carrying on to the next wp using your humanoid move to finished wait method.

I think the problem with your code isn’t compute time but it’s a bug with humanoid movetofinished in general. As you identified when u remove that line everything is fine.

Also you shouldn’t do while task.wait(1) do because you’re yielding before any code is actually being executed. So when the game starts. You wait for 1 second, then your while loop runs. Rather than the better approach of being able to start the path finding immediately without that first delay and then adding task.wait(1) at the end.