Finding What's Actually Slow with Unreal Insights: The Step After stat

Created: 2026-07-25

stat unit tells you Game is heavy, but not which code is heavy. How to record a trace with Unreal Insights, open a frame in Timing Insights, and drill through the call hierarchy until your own Blueprint appears — with a hands-on that narrows down with stat and confirms with Insights.

You brought up stat unit and Game reads 18 ms. So the CPU is the problem. And then you're stuck — because you still don't know which Actor, or which piece of logic, is responsible.

stat tells you where things are backed up, and that's where its job ends. Unreal Insights records an entire run so you can open a single frame afterward and read its contents function by function. This article covers recording a trace, reading Timing Insights, and drilling down the call hierarchy until you land on your own Blueprint.

Narrowing down with stat and then identifying the heavy work in Unreal Insights, with a soft blue clay figure

What You'll Learn

  • How stat and Insights divide the work
  • Recording a trace (from the editor, or via launch arguments)
  • 🚨 Surfacing your Blueprint names needs -statnamedevents
  • Opening a single frame in Timing Insights
  • Drilling the call hierarchy until your Blueprint appears
  • Memory Insights for what's eating memory
  • Hands-on: narrow down with stat, confirm with Insights

Sponsored

What stat Tells You, and What It Doesn't

The stat commands are for leaving a readout on screen and knowing the current state. stat unit gives you Frame / Game / Draw / GPU, which tells you where the backup is.

What it can't tell you is what comes next.

A comparison: stat stops at "Game is 18 ms" while Insights drills down to "BP_Enemy's Tick is 12 ms"
stat commandsUnreal Insights
What you seeWhich thread is backed upWhich function took how long
GranularityPer thread, per systemPer individual call
When you lookLive, while runningRecorded, opened afterward
Good atNarrowing downConfirming

So the two don't compete. The order is narrow down with stat, confirm with Insights.

If stat unit's Game is large, CPU-side game logic is heavy. Open Insights and those 18 ms break out into a bar per function.


Recording a Trace

Unreal Insights splits the recording tool from the viewing tool. You record while the game runs, then open the resulting file (.utrace) afterward.

Launching with -statnamedevents, recording with the CPU and Frame channels, saving a .utrace, then opening it in Unreal Insights to see BP_Enemy_C

The easiest route is the trace button in the editor's status bar, bottom right. Press it to start recording, press again to stop. Stopping writes out a .utrace file.

What you choose when recording are the channels — what gets captured. Recording everything is heavy, so narrow it to your purpose.

ChannelWhat it recordsWhen
CPUFunction calls and their durationsStart here. Anything CPU-heavy
GPURendering durationsWhen stat unit's GPU is large
FrameFrame boundariesAlways (it's the ruler on the timeline)
MemoryAllocations and freesMemory investigations (below)

CPU and Frame alone are enough to start. Turning on every channel makes the recording itself heavy, burying the load you came to measure.

Open the .utrace in the Unreal Insights app — a standalone application in the engine install. When you record from the editor, there's often a way to open it right from the same button.

Step away from the editor to measure. When precision matters, measure in Standalone Game or a packaged Development build rather than editor Play. Editor Play mixes in the editor's own rendering and tooling.

Get Your Own Names to Show Up

There's one thing to settle first. By default, the Blueprints you made may not appear by name at all.

A standard trace is dominated by engine-internal names. To surface something like BP_Enemy_C, you need to record with named events enabled.

Adding -statnamedevents to the launch arguments is the most reliable route.

MyGame.exe -statnamedevents

And crucially: before you start the real investigation, confirm that names appear at all.

  1. Launch with -statnamedevents
  2. Record for five seconds
  3. Open it in Insights and look for anything starting with BP_
  4. If there's nothing, revisit the launch arguments and channel settings

Skip this and you can't tell "there's no heavy work" from "the names just aren't showing." It takes thirty seconds, so always do it first.

How fine the detail gets depends on your setup. BP_Enemy_C may appear while Get All Actors Of Class inside it does not — that comes down to recording settings. Reaching the Blueprint name is enough. To narrow further, open that Blueprint and read the nodes, or add your own named events.

Sponsored

Reading Timing Insights

Once the .utrace is open, the tab you'll live in is Timing Insights.

The Timing Insights layout: pick a frame from the timeline at the top, and function bars nest in the tracks below

Three places to look.

AreaWhat it shows
Frame list, topEach frame's length as a bar. A bar that spikes is a hitch
Tracks, middleRows like GameThread / RenderThread. Calls appear as horizontal bars
Panel, bottomAggregates for the selection. Which work totals how many milliseconds

The basic move is to find a long frame at the top and select it. Staring at the average never finds a hitch. Go straight for the single longest bar.

Selecting a frame switches the tracks to that moment's contents. Those horizontal bars are the actual work.

  • Bar length = time that call took
  • A bar hanging below another = work called by it

So descending from the top layer walks you toward the culprit.


Drilling Down to the Heavy Work

This is the heart of the tool. Open the widest bar, then the widest one under that.

Expanding the widest bars reveals BP_Enemy's function under Tick, taking up most of the width

The loop is simple.

  1. Find the widest bar (you'll see broad names like FEngineLoop::Tick)
  2. Expand it and look at the layer below
  3. Pick the widest one again
  4. Repeat until a name you chose appears

When something like BP_Enemy_C or UpdateHealthBar shows up — something you named — that's your culprit. As long as you're still seeing engine-internal names, you haven't gone deep enough.

Many copies of the same name means count is the problem. Each may be short, but 200 BP_Enemy Tick entries add up. In that case, making the work faster matters less than calling it less often (→ Designing Without Tick).


Memory Insights

To investigate memory rather than time, you have to launch with the Memory channel enabled.

This differs from the CPU workflow. Memory recording has to be active from the instant the process starts, or it can't account for allocations made before that. Starting a recording from the UI later tells you nothing about what loaded at startup.

MyGame.exe -trace=default,memory

Specify it as a launch argument, play, then open that trace.

What you get is when, what, and how much was allocated. Two main uses:

  • What loads at startup — are assets you never use in memory? (→ Soft References and Async Loading)
  • Whether memory grows during play — Actors or Widgets you forgot to destroy

The Memory channel makes recording heavier, so capture it separately from CPU investigations. Time today, memory tomorrow.

Sponsored

Hands-On: Track Down a Hitching Level

A wave-defense with swarms of enemies, an open-field exploration section, a bullet-hell shooter. "It only hitches in this one situation" follows the same procedure in any genre. Let's narrow down with stat and confirm with Insights.

Building a reproducible case

To identify a cause you need something that reliably hitches. If you don't have one, build this.

ItemDetails
BP_EnemyAn Actor with a Static Mesh. On Event Tick, call Get All Actors Of Class (for BP_Enemy) and store the array's length in a variable
CountPlace 200 BP_Enemy in the level
EnvironmentLaunch Standalone Game with -statnamedevents (not editor Play)

Get All Actors Of Class walks the entire level — an expensive call. Having 200 actors call it every frame is guaranteed to hurt (it's also a staple of the common mistakes).

The flow from stat reporting Game at 18 ms, to opening the long frame in Insights, to expanding Tick and identifying BP_Enemy

Steps

  1. Launch Standalone with -statnamedevents and type stat unit in the console. Confirm Game is the outlier (Draw and GPU should be small)
  2. While it's hitching, start recording a trace. Channels: CPU and Frame
  3. Record about five seconds, stop, and open the saved .utrace in Unreal Insights
  4. First confirm names are showing. Anything starting with BP_ means you're set (if not, revisit the launch arguments)
  5. Open the Timing Insights tab and select the longest frame in the list at the top
  6. In the GameThread track, expand the widest bar, repeatedly
  7. When BP_Enemy_C lines up under Tick, you've found it. That's enough to know what to fix

Checking it

If it worked, most of the time stat unit attributed to Game is concentrated in BP_Enemy's Tick. 0.05 ms each, times 200, is 10 ms. What stat could only call "Game is heavy" now names which Actor is responsible.

Once you're at BP_Enemy_C, just open that Blueprint and read its Event Tick. Narrowing it to "this Actor is the heavy one" is enough — you can follow the contents by eye.

From here the fix picks itself: stop calling Get All Actors Of Class every frame — fetch it once into a variable, or drop Tick entirely. Then measure again the same way and confirm the Game number dropped.

Troubleshooting:

  • Only engine-internal names appear → Two possibilities: you launched without -statnamedevents, or you haven't expanded far enough. Check the launch arguments first, then keep picking the widest bar
  • Every frame is about the same length → That's not a hitch, it's consistently heavy. Same procedure; just pick any typical frame and descend
  • The numbers differ from the editor → That's correct. Standalone is closer to reality

Two things to take away.

  • Look at the longest single frame, not the average: hitches happen occasionally, so they vanish into averages. Aim straight at the spike in the frame list
  • Measure in a state where your own names appear: knowing FEngineLoop::Tick is heavy gives you nothing to act on. Adding -statnamedevents and reaching BP_Enemy_C is what defines the fix

If the fix is fewer things drawn, LOD and culling takes over; if it's constant spawning and destroying, object pooling does.


Bonus: Good to Know for Later

Anything but Shipping works. Traces record in Development builds too. To measure something close to the real thing, run a packaged Development build on the target hardware and record there.

Three launch arguments cover it. -trace=cpu,frame (what to record), -statnamedevents (surface your own names), and -trace=default,memory (record memory). When you want the cost of startup itself, the UI button is too late — start from arguments.

Trace files get big. A .utrace can hit hundreds of megabytes in seconds. Delete the ones you've finished with or they quietly eat your disk.

Make "measure, then fix" a habit. Fixing what you feel is slow misses the mark surprisingly often. Measure before, measure after. That round trip is the most reliable way to make progress on performance.


Summary

  • stat narrows down, Insights confirms. Use them in that order
  • Record with CPU and Frame first. Everything at once makes the recording itself heavy
  • 🚨 Surfacing your Blueprint names needs -statnamedevents. Confirm names appear before the real investigation
  • 🚨 Memory needs -trace=default,memory from launch. Starting later can't account for startup allocations
  • In Timing Insights, select the longest frame. Averages hide hitches
  • Descend until something you named appears (BP_Enemy_C and the like)
  • For precision, measure in Standalone or a Development build

An investigation that used to stop at "Game is large" now reaches a function name. What is your project doing during its longest frame?