home / blog / engine
Published: 24 May 2026 9 min read engine
Unreal Blueprint vs C++: the real cost I measured in ms

Unreal Blueprint vs C++: the real cost I measured in ms

The Blueprint VM is invisible in most scenes, but at tens of thousands of nodes per tick it cost us 7 ms. Here is the hybrid split, with numbers.

When 41,000 nodes run in a single tick

On a tower defence project in late 2023, frame time climbed from 11.2 ms to 19.4 ms once wave 14 started. In the editor everything looked acceptable; in a packaged Development build the gap was obvious. The top line in Unreal Insights was BlueprintTime, eating 7.1 ms on its own.

The culprit was the Event Tick inside BP_TowerBase. Every tower walked all 220 enemies with a ForEachLoop, computed a squared distance and picked the closest one. Twenty-four towers times 220 enemies, with a handful of operations per node, worked out to roughly 41,000 node executions per frame.

The problem was not that "Blueprint is slow". The problem was running an O(n·m) search inside a virtual machine every single frame. Moving that code to C++ does lower the cost, but the real mistake was algorithmic, and in the Unreal Blueprint vs C++ argument those two things get mixed up constantly.

There were three of us on the team then, and none of us was profiling the Blueprint side at all. As frame time slipped we looked at draw calls first, then shadow settings, then texture resolutions. After losing two days, opening Insights and reading the right line was all it took. The rest of this post is the set of rules I wrote down so I would not spend those two days again.

Where Blueprint performance becomes measurable

To get a number I ran a plain test: call an empty function 100,000 times. On the C++ side the total was 0.4 ms; the identical function in Blueprint took 6.8 ms. That works out to about 68 ns of overhead per call.

68 ns looks like nothing, and most of the time it is. For a 200-node door-opening flow or the logic behind an inventory screen the cost stays under the noise floor; below roughly 2,000 nodes per frame the total impact never passed 0.15 ms in any of my measurements.

The point where it stops being noise is clear: tens of thousands of nodes per frame. The rough threshold I use is this — if a Blueprint runs every frame and contains a loop, that logic is now a C++ candidate. For flows triggered by one-off events, VM overhead is not worth discussing.

There is also a measurement trap. In PIE, Blueprint calls look more expensive than they really are because of the debug instrumentation, so deciding from an editor number is misleading. I do not move anything to C++ before checking a packaged Development build with stat game and Insights.

Hybrid workflow: core in C++, tuning in Blueprint

Target selection, damage calculation, cooldown tracking and the state machine moved into AOFKTowerBase as C++. The target search no longer runs per frame; it runs on a 0.2 second timer over a spatial grid. What stayed in Blueprint: effect timing, audio triggers, UI feedback and the numbers the designer keeps tweaking.

Frame time went from 19.4 ms to 11.8 ms and BlueprintTime dropped from 7.1 ms to 0.9 ms. Not all of that came from the VM: roughly 5 ms belongs to the algorithm change and 2.6 ms to the move to native code. Without that split, saying "C++ made it 40% faster" would be dishonest.

Two alternatives were rejected. Going full C++ was technically the fastest, but a designer testing a single damage value had to sit through a 90 second compile and an editor restart; for someone running 40 experiments a day that is not acceptable. Keeping everything in Blueprint and just disabling tick achieved nothing, because it did not remove the loop that caused the cost.

The question I ask when drawing the line is simple: does changing this value require a compile? If it does, and a designer changes it several times a week, that value belongs in Blueprint or in a data asset. We keep the tower balance table as a UDataAsset: C++ reads it, the designer edits it in the editor, and nobody waits on a build.

AOFKCharacter.h
#pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "AOFKCharacter.generated.h" UCLASS() class OFKGAME_API AOFKCharacter : public ACharacter { GENERATED_BODY() public: // Designer-tunable value, read-only in Blueprint so the rule stays in C++. UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Combat", meta = (ClampMin = "0.05", UIMax = "2.0")) float DashCooldown = 0.35f; UFUNCTION(BlueprintCallable, Category = "Combat") bool CanDash() const; protected: // C++ decides when it fires; Blueprint decides how it looks and sounds. UFUNCTION(BlueprintImplementableEvent, Category = "Combat") void OnDashStarted(float Cooldown); private: float LastDashTime = -1000.f; };
cppAOFKCharacter.h

Exposing the right surface with UPROPERTY

The quality of a hybrid setup depends on exactly what C++ exposes to Blueprint. Tunable numbers go out as UPROPERTY(EditAnywhere, BlueprintReadOnly): the designer can change the value in the editor but cannot write to it at runtime. Adding meta = (ClampMin, UIMax) removes half of the "someone typed 0 by accident" bugs before they ever exist.

On the function side, the difference between three specifiers matters. BlueprintCallable is Blueprint asking C++ a question; BlueprintImplementableEvent is C++ telling Blueprint "this just happened, you handle how it looks"; BlueprintNativeEvent gives a default implementation in C++ that Blueprint may override when it needs to. The rule is short: C++ decides when, Blueprint decides how it looks.

The most common mistake I see is stamping BlueprintReadWrite on every field. Once it is open, designers start writing to state variables and the invariants you guard in C++ break quietly; for us that surfaced as an ammo counter going negative. The second trap is renaming: changing a UPROPERTY name silently breaks Blueprint references, so we never rename a field without adding a Core Redirects entry.

There is a compile-time side to this too. Touching a UPROPERTY in a header rebuilds everything that includes that header, which reached 4 minutes for us on a decent machine. Grouping the frequently changed settings into one small separate struct sped up the designer's loop and visibly cut how many full rebuilds we did per day.

What changed after nativization was removed

Around 4.26 some teams leaned on Blueprint nativization, and we tried it for a while. The measured gain in heavy scenes was about 1.6 ms; in exchange, packaging took 18 minutes longer and we hit three bugs that only appeared in nativized builds. UE5 removed the option entirely.

The practical consequence is that there is no late-stage escape hatch where the compiler saves you. Having the hot path in C++ is not an optimisation step any more, it is an architectural decision made at the start of the project. Treating UE5 C++ as a patch you bolt on later is what catches teams mid-production.

There is an upside too. While nativization existed, people wrote heavy logic in Blueprint and planned to "nativize it later", and later never arrived. With the option gone, drawing the boundary up front became mandatory, and that left us with a cleaner codebase over time.

What we put in nativization's place is measurement. Before every release we capture an Insights trace from a packaged build, and when BlueprintTime goes past 1 ms we track down which Blueprint is responsible. The check takes fifteen minutes. Over the past year it caught a serious regression three times before it shipped.

Merge conflicts once the team grows

With four people, Blueprint being a binary asset was not a problem. At eleven people, two or three times a week two people touched the same .uasset, and a .uasset cannot be merged; whoever lost redid their work. We put the time lost that way in one sprint at roughly two days.

We set up mandatory checkout and exclusive locks in Perforce. The conflicts stopped and waiting replaced them: while a Blueprint is locked, the second person either waits or switches tasks. C++ does not have that problem — text merges line by line and can actually be read in a pull request. Reviewing a Blueprint, in practice, means sending screenshots.

I apply four rules to Unreal Engine development today. No logic that runs every frame stays in Blueprint, and tick is off by default on every Blueprint we create. If a Blueprint passes 150 nodes, a piece of it moves out to C++. And if two people need to edit the same Blueprint within a single sprint, that logic already belongs in C++.

The rules are about team throughput as much as performance. Use Blueprint for fast designer iteration and C++ for the backbone of the system, and draw that line before the first line of code. Moving later is always more expensive — our bill was a three week refactor.

← All posts