Godot Multithreading Tutorial: Thread Safety and WorkerThreadPool

Heavy calculations can bring a Godot game to a standstill. When procedural generation, pathfinding, or data parsing runs on the main thread, rendering and input have to wait their turn, and players feel it as stutters or a frozen frame. Godot multithreading solves this by moving expensive work onto background threads, and in this tutorial you’ll learn how to do it safely by building a benchmark that measures the difference for yourself.

This tutorial is adapted from Zenva’s structured game development training, which has helped a community of over 1,000,000 learners and developers gain digital skills through courses, interactive apps, eBooks, and technical tutorials. You’ll follow the same guided approach here: first the theory and thread-safety rules, then a hands-on test with real numbers.

Before starting, make sure you have Godot installed and are comfortable with the basics, such as creating scenes and attaching scripts.

https://youtu.be/68Rn-EfJlio

Download Project Files

The files and the full project are available through the course included in the Intermediate Godot Mini-Degree if you’d like to follow along with the completed project.

Make a Complete Card Battler in Godot e1788415191322 - Godot Multithreading Tutorial: Thread Safety and WorkerThreadPool
FREE GODOT COURSE
LEARN GODOT, UNITY, UNREAL & MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Godot Multithreading: Sequential and Parallel Execution

Code normally runs sequentially: each operation finishes before the next one begins, much like following a recipe one step at a time. Multithreading divides work into separate threads that can execute simultaneously across multiple CPU cores.

That is valuable for expensive calculations or large amounts of data. Moving work away from the main thread prevents rendering, input processing, and other visible gameplay from pausing while those calculations finish. Common background tasks include procedural generation during a loading screen, parsing downloaded data, streaming resources, and calculating AI navigation paths.

Understanding Thread Safety

Parallel execution is not suitable for every operation. Multiple threads interacting with the same engine state can create errors, inconsistent data, or crashes. In particular, Godot’s scene tree is not thread safe and should not be accessed or modified directly from a background thread.

Generally Safe Operations

  • Accessing singletons and autoload data
  • Using NavigationServer features designed for background path calculations
  • Loading resources without modifying them

Operations to Avoid

  • Adding, removing, or changing Nodes in the scene tree
  • Most rendering and physics operations
  • Modifying shared resources

Godot settings can enable some rendering-server and physics-server work from additional threads, but these advanced options should be used only when their synchronization requirements are fully understood.

Using the Thread Class

The Thread class runs a function on a dedicated thread until that function returns. It gives you direct control over the thread lifecycle and works well for a continuous background process, such as listening for network packets.

That control also creates responsibility. Your game must signal the process to stop and wait for the thread to finish when its owning Node exits. Missing cleanup can leave work running indefinitely and cause crashes or thread leaks.

Using WorkerThreadPool

WorkerThreadPool is a singleton that manages a set of worker threads for you. Godot handles starting and stopping those threads, which makes this approach well suited to batch processing.

A group task receives a function and an iteration count. The pool invokes that function for each index, allowing a large array of independent items to be processed in parallel. For example, one group task could calculate updated paths for many enemies, with each invocation using its index to select an enemy.

The task returns an identifier that can be used to wait for group completion. This provides less lifecycle control than a raw Thread, but it requires much less manual management.

Choosing an Approach

Use a dedicated Thread when a specific long-running process needs explicit lifecycle control. For most parallel game-development workloads, especially repetitive data processing, WorkerThreadPool is the recommended starting point.

With the theory covered, it’s time to see the difference in practice. You’ll build a benchmark that processes an intentionally expensive operation sequentially, then process the same input with WorkerThreadPool so the two timings can be compared fairly.

Preparing the Benchmark Scene

Start by creating a Multithreading folder with Scenes and Scripts subfolders. Add a simple Main scene containing a Node named Multithreading, then save the scene in Multithreading/Scenes.

Godot FileSystem showing the Multithreading project folder and Create New menu.

Attach a new script named multithreading_test.gd and save it in Multithreading/Scripts.

Godot Attach Node Script dialog showing the multithreading script path.

Defining the Workload

Next, configure how many values the benchmark will process and how much work each value requires. Packed arrays store the input and both result sets with a compact memory layout:

const ITERATION_COUNT = 50
const WORK_PER_ITERATION = 100000

var data : PackedFloat64Array
var results_sequential : PackedFloat64Array
var results_threaded : PackedFloat64Array

Set up the input data by resizing each array and filling data with random values between 0 and 10,000:

func _setup_data ():
	data.resize(ITERATION_COUNT)
	
	for i in ITERATION_COUNT:
		data[i] = randf() * 10000.0
	
	results_sequential.resize(ITERATION_COUNT)
	results_threaded.resize(ITERATION_COUNT)

Creating an Expensive Operation

The benchmark needs enough CPU work to produce a visible timing difference. Repeated sine, cosine, and square-root calculations provide an intentionally heavy operation:

func _expensive_operation (value : float) -> float:
	for i in WORK_PER_ITERATION:
		value = sin(value) * cos(value) + sqrt(value)
	
	return value

Each of the 50 input values runs through this calculation 100,000 times.

Running the Sequential Test

Record the start time in milliseconds, process each value one after another, and subtract the start time from the final clock value:

func _run_sequential ():
	var start_time : int = Time.get_ticks_msec()
	
	for i in ITERATION_COUNT:
		results_sequential[i] = _expensive_operation(data[i])
	
	var elapsed_time : int = Time.get_ticks_msec() - start_time
	print(str("Sequential time: ", elapsed_time, "ms"))

Call the setup and sequential functions when the Node becomes ready:

func _ready ():
	_setup_data()
	_run_sequential()

Reading the Result

Run the scene and inspect Godot’s Output panel. The example run completed in approximately 563 milliseconds, although the exact value depends on the computer.

Godot Output panel reporting a sequential benchmark time of 563 milliseconds.

Because every operation runs on the main thread, the game remains blocked until the benchmark finishes. Keep that number in mind, because the threaded version will process the same input next.

Submitting a Group Task

Create _run_threaded() with the same timing structure used by the sequential test. Submit a group task by passing the worker function and the number of elements to process:

func _run_threaded ():
	var start_time : int = Time.get_ticks_msec()
	
	var task_id : int = WorkerThreadPool.add_group_task(_threaded_task, ITERATION_COUNT)
	WorkerThreadPool.wait_for_group_task_completion(task_id)
	
	var elapsed_time : int = Time.get_ticks_msec() - start_time
	print(str("Threaded time: ", elapsed_time, "ms"))

add_group_task() schedules _threaded_task() once for every index from zero to ITERATION_COUNT minus one. The returned task ID identifies this batch and is passed to wait_for_group_task_completion() to synchronize before measuring the final time.

Processing Each Index

The worker function receives the automatically assigned index. Use it to read one input value and write the corresponding threaded result:

func _threaded_task (index : int):
	results_threaded[index] = _expensive_operation(data[index])

Each invocation performs the same expensive calculation used by the sequential test, but WorkerThreadPool can distribute independent iterations across multiple workers.

Running Both Tests

Call the threaded benchmark immediately after the sequential benchmark so both use the same prepared data and workload:

func _ready ():
	_setup_data()
	_run_sequential()
	_run_threaded()

The comparison frame recorded a sequential time of 562 milliseconds and a threaded time of 309 milliseconds. Exact timings vary by processor and system load, but the same operation completed faster when distributed across workers.

Godot Output panel comparing a 562 millisecond sequential run with a 309 millisecond threaded run.

Applying WorkerThreadPool Appropriately

Parallel workers are useful for large batches of independent data, such as recalculating paths for many enemies. They are not a universal performance switch. Background tasks must avoid unsafe scene-tree access, resource modification, and unsupported rendering or physics operations.

Choose multithreading when a workload can be separated into independent, thread-safe calculations. Keeping those calculations away from the main thread can reduce gameplay stutters while preserving responsive rendering and input.

Conclusion

You now have a working benchmark that shows multithreading in action. Along the way, you covered:

  • How multithreading divides work across CPU cores
  • Which Godot operations are safe from background threads
  • When to choose a Thread versus WorkerThreadPool
  • How to submit a group task and wait for its completion
  • How to measure and compare sequential and threaded timings

From here, you can apply the same pattern to your own expensive workloads, such as pathfinding or procedural generation, and keep your game responsive while they run.

If you want to keep building your Godot skills with a structured, project-based path, the Intermediate Godot Mini-Degree covers this topic and other intermediate Godot techniques in depth.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image - Godot Multithreading Tutorial: Thread Safety and WorkerThreadPool

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.