Godot MessageQueue: How Inter-Object Communication Works in the Engine Core

The MessageQueue is Godot’s thread-local singleton that enables deferred inter-object communication by storing callable requests, notifications, and property sets in a page-based buffer that gets flushed during the main loop.

The MessageQueue is a fundamental subsystem in the godotengine/godot repository that solves the problem of safe, order-preserving communication between objects across different threads and engine phases. Understanding this mechanism is essential for advanced engine development and debugging deferred call behavior.

What Is the Godot MessageQueue?

At its core, the MessageQueue is a thread-local singleton that owns a CallQueue instance. Rather than executing method calls immediately, the queue stores messages—binary descriptors representing deferred operations—until the engine reaches a safe point in the main loop.

The architecture relies on paged memory allocation. Each CallQueue maintains a list of fixed-size pages (PAGE_SIZE_BYTES = 4096), where each page stores raw binary data for one or more Message structures plus their serialized Variant arguments. This design minimizes allocation overhead during high-frequency deferred calls.

Core Message Types

The queue supports three distinct operation types defined in core/object/message_queue.h:

  • TYPE_CALL – Stores a Callable and its arguments for later invocation via Callable::callp().
  • TYPE_NOTIFICATION – Queues an Object::notification() call with a specific notification constant.
  • TYPE_SET – Defers a property assignment via Object::set().

How MessageQueue Enables Inter-Object Communication

Inter-object communication through the MessageQueue follows a strict lifecycle: packing, queuing, flushing, and execution. This path ensures that calls made during physics processing, rendering, or from secondary threads execute safely during the main loop iteration.

The Deferred Call Path

When a script calls call_deferred("method_name", args) or C++ code invokes Object::call_deferred(), the request enters Object::_call_deferred_bind() in core/object/object.cpp. This function:

  1. Constructs a Callable object targeting the specific object ID and method name.
  2. Packages the arguments into a Variant array.
  3. Pushes the callable to the thread-local MessageQueue via push_callable().

Message Flushing and Execution

During each frame, the engine calls MessageQueue::flush() (defined in core/object/message_queue.cpp). The flush process:

  1. Locks the queue mutex to prevent concurrent modifications.
  2. Iterates through all allocated pages, extracting each Message header.
  3. Reconstructs the original arguments from the page’s binary buffer.
  4. Dispatches the operation:
    • For TYPE_CALL: Invokes _call_function() which executes Callable::callp().
    • For TYPE_NOTIFICATION: Calls Object::notification() directly.
    • For TYPE_SET: Invokes Object::set() with the deferred property value.
  5. Clears the queue pages, resetting the buffer for the next frame.

The mutex implementation uses LOCK_MUTEX/UNLOCK_MUTEX macros that skip locking when operating on the thread-singleton queue, preventing deadlocks when the same thread re-enters the queue during a flush.

Thread Safety and Memory Architecture

The MessageQueue architecture separates thread-local and global singleton instances. MessageQueue::get_singleton() returns thread_singleton for the current thread or main_singleton for the primary engine thread. This design allows worker threads to queue deferred calls that execute on their owning threads without cross-thread contention.

Memory management uses a page pool system. When a page fills (exceeding 4096 bytes), the queue allocates a new page from the pool. During flush, pages return to the pool rather than being freed, eliminating allocation overhead during steady-state engine operation.

Practical Code Examples

GDScript: Using call_deferred

The most common high-level interaction with the MessageQueue occurs through GDScript’s call_deferred method:

extends Node

var health = 100

func _ready():
    # Schedule damage application after the current frame

    call_deferred("apply_damage", 10)

func apply_damage(amount):
    health -= amount
    print("Health reduced to: ", health)

Under the hood, this translates to Object::_call_deferred_bind() in core/object/object.cpp, which pushes a TYPE_CALL message onto the thread-local queue.

C++: Direct Queue Access

For engine developers or GDExtension authors, direct queue manipulation provides finer control:

#include "core/object/message_queue.h"

void schedule_delayed_operation(Object *target) {
    Callable callable(target, "do_something");
    Variant arg = 42;
    
    // Push to the global MessageQueue
    MessageQueue::get_singleton()->push_callable(callable, arg);
}

This bypasses the Object API and directly appends a message to the CallQueue pages.

Posting Notifications

The engine uses TYPE_NOTIFICATION messages for deferred event distribution:

// Post a custom notification to be handled next frame
int NOTIF_CUSTOM_EVENT = 1000;
MessageQueue::get_singleton()->push_notification(
    target->get_instance_id(), 
    NOTIF_CUSTOM_EVENT
);

When flushed, this invokes target->notification(NOTIF_CUSTOM_EVENT), allowing objects to respond to events during the main loop iteration.

Thread-Safe Property Sets

For modifying properties from worker threads without immediate synchronization:

// Thread-safe deferred property modification
StringName prop_name = "position";
Vector2 new_position = Vector2(100, 200);

MessageQueue::get_singleton()->push_set(
    target->get_instance_id(),
    prop_name,
    new_position
);

The queue stores a TYPE_SET message that executes target->set("position", new_position) during the next flush cycle.

Key Source Files

Understanding the MessageQueue requires familiarity with three primary files in the Godot repository:

  • core/object/message_queue.h – Defines the CallQueue class, Message structure, page constants (PAGE_SIZE_BYTES = 4096), and the MessageQueue singleton interface.
  • core/object/message_queue.cpp – Implements page allocation, message packing (push_callablep), mutex handling (LOCK_MUTEX/UNLOCK_MUTEX), and the flush logic that dispatches queued operations.
  • core/object/object.cpp – Contains the high-level API entry points including Object::call_deferred() and Object::_call_deferred_bind(), which bridge script-level deferred calls to the underlying queue system.

Summary

  • The MessageQueue is a thread-local singleton managing deferred operations through a CallQueue instance.
  • It stores three message types—TYPE_CALL, TYPE_NOTIFICATION, and TYPE_SET—in fixed 4096-byte pages to minimize allocation overhead.
  • Deferred communication flows from Object::call_deferred() through Object::_call_deferred_bind() into MessageQueue::push_callable(), finally executing during MessageQueue::flush() in the main loop.
  • Thread safety is ensured through mutex protection with re-entrancy guards, allowing the same thread to safely flush while pushing new messages.
  • Direct C++ access via push_callable(), push_notification(), and push_set() provides engine developers with fine-grained control over inter-object communication timing.

Frequently Asked Questions

What is the difference between call_deferred and a regular method call in Godot?

A regular method call executes immediately on the current thread and stack, while call_deferred serializes the callable and arguments into a MessageQueue entry that executes during the next main loop iteration. This deferred approach prevents re-entrancy issues during scene tree modifications and ensures thread-safe communication when calling from worker threads to the main thread.

How does Godot's MessageQueue handle thread safety?

The MessageQueue uses a Mutex protected by LOCK_MUTEX and UNLOCK_MUTEX macros to serialize access to the CallQueue. However, when operating on the thread-local singleton (thread_singleton), the macros skip locking to prevent deadlocks during re-entrancy—allowing the same thread to push messages while flushing the queue. For cross-thread communication, the global main_singleton requires full mutex acquisition.

What are the performance implications of using call_deferred?

Each deferred call allocates space in a 4096-byte page within the CallQueue, copying the Callable and Variant arguments into contiguous memory. While this avoids per-call heap allocation overhead through page pooling, excessive deferred calls can still consume significant memory bandwidth during the flush phase. For high-frequency operations (e.g., every frame), direct method calls are preferred; deferred calls are optimized for sporadic, timing-critical, or cross-thread communication.

Can I use MessageQueue directly in GDScript?

No, the MessageQueue API is not exposed to GDScript. GDScript users interact with the system through high-level methods like call_deferred(), set_deferred(), and notify_deferred() (via Object methods). Direct access to MessageQueue::push_callable(), push_notification(), and push_set() is only available in C++ engine code or GDExtension plugins where you include core/object/message_queue.h.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →