Quick answer: Burst rejects any IL instruction that touches a managed reference — not just obvious ones like new List<T>(), but also string formatting, boxing to object, virtual interface calls, and lambda captures. Open the Burst Inspector, find the red diagnostic, and fix the specific boxing site. For ISystem, switch to SystemAPI.Query and avoid EntityManager methods.
You decorated a method with [BurstCompile] and the editor starts yelling: “BC1091: Managed object references are not supported in Burst” with a stack trace pointing at a line that is, as far as you can tell, pure value types. This is the single most common Burst frustration, and the fix always comes down to finding the one hidden allocation or virtual dispatch that the C# compiler inserted behind your back.
How Burst Sees Your Code
Burst compiles IL instructions, not C# source. The C# compiler frequently emits box opcodes, delegate construction, or virtual dispatch for code that looks innocent in the editor. Burst walks every call site reachable from your [BurstCompile] method and fails the first time it hits:
box: converting a value type toobject(e.g. passing an int toDebug.Log).newobjof a reference type:new StringBuilder(),new List<T>(), closures.callvirton an interface that is not[BurstCompatible]or a class method that has not been resolved to a concrete type.ldstrin certain contexts: string literals are fine, but string concatenation callsString.Concatwhich is managed.
Read the Burst Inspector
Open Jobs → Burst → Open Inspector. Find your method in the left panel, select it, and read the output. If it compiled, you see assembly. If it failed, you see a red diagnostic like:
BC1091: Loading from a managed type `System.String` is not supported.
at Assets/Scripts/MovementSystem.cs(42): Debug.Log("Tick " + time);
That tells you line 42 has a hidden String.Concat call. The fix is Unity.Logging or FixedString:
using Unity.Collections;
using Unity.Logging;
[BurstCompile]
public void OnUpdate(ref SystemState state) {
var time = state.World.Time.ElapsedTime;
FixedString64Bytes msg = default;
msg.Append("Tick ");
msg.Append(time);
Log.Info(msg);
}
SystemBase vs ISystem
A big source of confusion: SystemBase.OnUpdate runs on the main thread and is allowed to contain managed calls. Only the inner Entities.ForEach().WithBurst() lambda must be Burstable. You can therefore freely call Debug.Log inside OnUpdate of a SystemBase but not inside its ForEach lambda.
ISystem is different. When you put [BurstCompile] on the struct and on OnUpdate(ref SystemState state), the entire method body is Burst-compiled, including every call. That is why code ported from SystemBase to ISystem often explodes.
[BurstCompile]
public partial struct MovementSystem : ISystem {
[BurstCompile]
public void OnUpdate(ref SystemState state) {
// WRONG: EntityManager.GetName boxes and calls managed
// var name = state.EntityManager.GetName(entity);
// RIGHT: iterate via SystemAPI.Query
foreach (var (transform, velocity) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>()) {
transform.ValueRW.Position += velocity.ValueRO.Value * SystemAPI.Time.DeltaTime;
}
}
}
Hunt Down Hidden Managed Calls
Here is a checklist for the most common offenders, ordered by frequency in real codebases:
- String operations. Any
"a" + b,string.Format, orToString()on a value type. Replace withFixedString. - Debug.Log. Takes
objectso all arguments box. Replace withUnity.Loggingor guard with#if UNITY_EDITORand accept it is not Burst-compiled in that branch. - Lambda captures. Capturing a class field in an
Entities.ForEachcreates a closure class. Copy the field to a local before the lambda. - Interface calls via boxed struct. Calling
IMyInterface m = new MyStruct(); m.Do()boxes. Use generic constraintswhere T : struct, IMyInterfaceinstead. - try/catch/finally with managed exceptions. Burst supports limited exceptions; avoid custom exception types.
- params arrays. Every call allocates an array. Use fixed overloads or
FixedList.
Use the Safety Attributes
Mark structs with [BurstCompile] and methods you want to guarantee Burst-compiled with [BurstCompile(CompileSynchronously = true)] during development. Synchronous compilation blocks the editor on each change but surfaces errors immediately rather than on first call.
Set [BurstCompile(DisableSafetyChecks = false, OptimizeFor = OptimizeFor.Performance)] on hot path methods. With safety checks enabled, Burst will also tell you about other classes of issue like out-of-bounds NativeArray access that might otherwise look like managed-code issues.
“Burst does not hate your code. It hates the IL the C# compiler generated from your code. Learn to read IL and you stop fighting it.”
Verifying the Fix
A successful Burst compile produces native assembly in the Inspector. Look for your loop unrolled into vectorized instructions (vmovups, vfmadd231ps on x86). If you see call into managed glue, something is still being bailed out. Re-check the red diagnostic.
Related Issues
If Burst compiles but performance is worse than expected, see Burst Vectorization Not Happening. For jobs that pass safety checks but corrupt data, read DOTS Data Race Missing Dependency.
Burst Inspector + FixedString + ISystem/SystemAPI = zero managed-code errors on hot paths.