Unity DOTS dependency management is the mechanism that keeps safe parallel execution possible across the Entity Component System (ECS), the C# Job System, and Burst-compiled code. At its core it is a graph of JobHandle objects: every job you schedule returns a handle, and every read or write of component data through a SystemAPI or EntityCommandBuffer records which handles must complete before the next job can run. If you understand this graph, you understand DOTS performance; if you ignore it, you get race conditions, Unity's 'job has not completed' exceptions, or silent main-thread stalls that erase all your parallelism gains.
The Direct Answer: JobHandles Are the Dependency Model
Also worth reading: How do you optimize bandwidth in Unity multiplayer games using interest management? · Unity NGO vs Netcode for Entities: which multiplayer framework should my studio pick in 2026? · How does Unity NetCode ghost snapshot compression actually work, and how much bandwidth can it save?
In Unity DOTS, dependency management means tracking and combining JobHandle instances so that jobs reading and writing the same data never overlap unsafely. When a system schedules an IJobEntity, IJobChunk, or raw IJob, Unity's Entities package automatically passes in a combined dependency for every component type the job touches. That combined dependency is itself a JobHandle produced by Unity.Jobs.JobHandle.CombineDependencies, and when the job finishes, the system writes the returned handle back into the EntityDataManager's per-component-type dependency list.
This is why two systems that both write Position will serialize automatically: the second system's scheduled job receives the first system's write handle as its input dependency. Conversely, two systems that only read Position can run fully in parallel because read-only access does not create ordering constraints. The safety system lives in the low-level unsafe layer (Unity.Collections.NativeContainer safety checks), and the Entities layer builds on top of it by maintaining one dependency per component type per world.
The practical consequence: most simple projects need almost no manual dependency code. You call Dependency = job.Schedule(Dependency) inside OnUpdate, and the framework threads everything together. Manual management becomes necessary when you interleave structural changes, use EntityCommandBuffers with custom playback order, share NativeContainers between systems, or want to overlap independent work explicitly.
Why DOTS Needs Explicit Dependencies at All
Traditional Unity MonoBehaviour code runs on the main thread sequentially, so ordering is implicit in script execution order. DOTS throws that away: potentially hundreds of thousands of entities are processed by jobs spread across worker threads (Unity's default job worker count equals the logical core count minus one, so a 12-core machine gets roughly 11 workers). Without a formal dependency model, a job writing Health could race a job reading Health mid-frame, producing torn state that is nearly impossible to debug.
The JobHandle model solves this cheaply. A handle is a lightweight struct pointing at a node in the job scheduler's internal graph; combining handles costs nanoseconds, and the scheduler only blocks a worker when a real hazard exists. This is the same design philosophy behind build tools like SCons, CMake, and newer entrants such as Pcons — declare what depends on what, let the tool compute a safe parallel schedule. Unity applied that build-system insight to runtime simulation loops.
There is a second reason: Burst compilation. Burst-compiled jobs run 10x to 100x faster than managed equivalents for numeric workloads, but they cannot safely touch managed memory or arbitrary main-thread state. The dependency graph guarantees a Burst job sees component data only after prior writers finish, which lets Unity keep safety checks off the hot path while still preventing corruption. In editor development builds, the safety system additionally validates every container access against the recorded dependencies, throwing descriptive exceptions like 'NativeArray has been deallocated' or 'job was not declared to read/write this component' — errors that are annoying during development and lifesavers in production.
How Automatic Dependency Injection Works Step by Step
When your system's OnUpdate runs, the Entities framework assembles a dependency set from three sources. First, the system's own previously registered dependencies (stored in the system's Dependency property). Second, the read/write sets declared by the query you iterate — calling SystemAPI.Query<T> or using [WithAll] attributes tells the framework which component types are involved. Third, any handles you manually assign to the Dependency property before scheduling.
A typical update looks like this: you schedule an IJobEntity that reads Velocity and writes Position. The framework combines the current write-dependency for Position and read-dependency for Velocity into one handle, passes it to Schedule, and reassigns the returned handle back to both component types' registries after the call. The next system that touches either type inherits that handle automatically. CompleteDependencyBeforeRead-style bookkeeping happens lazily: if a system later needs main-thread access via SystemAPI.GetComponent, the framework calls Dependency.Complete() internally, stalling the main thread until outstanding writers finish.
The cost profile matters here. A main-thread Complete() on a heavy simulation job can block for several milliseconds, which is why Unity's guidance since Entities 1.0 (released September 2023, with continued updates through 2025–2026) emphasizes keeping systems jobified end-to-end. Teams that measure with the Profiler's Flow view typically find that 60 FPS budgets allow about 16.6 ms per frame, and a single unnecessary Complete() can consume 2–5 ms of that on a mid-range CPU. Structural changes are the other automatic sync point: adding or removing components forces playback of recorded EntityCommandBuffer operations, which cannot run concurrently with jobs touching entity metadata.
Practical Steps for Managing Dependencies Correctly
Start by letting automation do its job. Write systems as pure job schedulers: query, schedule, return. Avoid calling .Complete() anywhere except where you genuinely need main-thread results, such as feeding UI or network serialization. Profile with Window > Analysis > Profiler and enable the Job Debugger (Jobs > Leak Detection and Full Stack Traces) during development only, since full safety checks can cut job throughput by 20–50%.
When you need manual control, follow these patterns. To run two independent jobs in parallel, schedule them both from the same incoming Dependency without chaining: jobA.Schedule(Dependency) and jobB.Schedule(Dependency), then set Dependency = JobHandle.CombineDependencies(handleA, handleB). To sequence them deliberately, pass handleA into jobB's Schedule call. For shared scratch buffers, allocate a NativeArray with Allocator.TempJob, pass it to both jobs, and make sure the second job's dependency includes the first — otherwise the safety system throws immediately in the editor.
For structural changes, prefer EntityCommandBuffer.ParallelWriter with a sort key derived from the chunk index and entity index, then play back the ECB in a later sync-point system. Since Entities 1.x, ISystem structs with [BurstCompile] attributes give lower overhead than classic SystemBase classes — benchmarks commonly show 1.5x to 3x less per-system overhead for systems iterating many small queries — but ISystem requires unmanaged component types and careful handling of managed references via UnsafeVariablePointer or lookup patterns.
Finally, register external dependencies explicitly. If a native plugin or async operation outside the job system produces data your jobs consume, wrap completion in a custom JobHandle via JobHandle's extension points or simply Complete before scheduling dependent work. Undocumented cross-world sharing is a common source of bugs: each World maintains separate dependency registries, so passing a NativeContainer between worlds requires completing the source world's dependencies first.
Comparing Your Options: Automatic vs Manual vs Sync Points
| Aspect | Automatic (default) | Manual CombineDependencies | Main-thread Complete() |
|---|---|---|---|
| Code complexity | Minimal, near-zero boilerplate | Moderate, requires graph reasoning | Lowest to write, highest to debug |
| Parallelism | Good for isolated systems | Best achievable overlap | None during the stall |
| Typical frame cost | Sub-millisecond overhead | Same overhead plus better overlap | 2–10 ms stalls common |
| Risk of races | Very low (framework-managed) | Low if correct, high if wrong | Eliminated by definition |
| Best used for | Standard gameplay systems | Overlapping AI + animation + physics | UI reads, save/load, network send |
| Debuggability | Excellent with safety checks | Requires Profiler flow analysis | Obvious but expensive |
Common Mistakes and How They Bite You
The most frequent error is calling .Complete() out of habit. Developers porting MonoBehaviour habits add Complete() after every schedule, serializing the entire frame and then concluding 'DOTS is slow.' Measure first: if your Profiler shows Worker threads idle while the main thread runs system code, you have premature sync points, not a DOTS problem.
Second is forgetting that reading a component also creates a dependency. Two systems that both read Health are fine, but a system that reads Health while another writes it will be serialized — and developers sometimes 'fix' perceived slowness by marking access [WriteOnly] incorrectly or bypassing safety with NativeDisableParallelForRestriction, introducing real races that only manifest on specific hardware. Never disable safety attributes without a profiler-backed reason and a stress test.
Third is mishandling EntityCommandBuffer ordering. ECB commands recorded in parallel jobs execute in sort-key order at playback; using identical sort keys for commands that depend on each other (instantiate then set component) causes missing-entity errors. Use the standard (chunkIndex, indexInChunk) pair or an explicit increasing counter.
Fourth is ignoring the frame-end implicit sync. Any job still running when the frame ends is completed implicitly before rendering, which hides sloppy scheduling in small scenes and then explodes at scale. Fifth is cross-container aliasing: passing the same NativeList to a writer and a reader without chaining handles works in the editor only because safety checks mask timing differences; in release builds with safety disabled, it corrupts memory intermittently — the worst class of bug to reproduce.
When to Act: Adoption Timing and Team Readiness
If you are starting a new project in 2026 targeting PC, console, or high-entity-count mobile genres (survival, RTS, large-scale multiplayer simulations), adopting DOTS now is reasonable: Entities, Physics, Netcode, and Transforms have been stable at 1.x for over two years, documentation is mature, and the ecosystem of samples (Megacity, EntityComponentSystemSamples repo) covers production patterns. Budget four to eight weeks for a team new to DOTS to become productive with dependency reasoning — that learning curve is the real cost, not licensing.
If you maintain an existing GameObject-based title, retrofitting DOTS wholesale rarely pays off. Instead, identify the top three CPU hotspots via profiling; if they involve more than roughly 5,000 simultaneously simulated entities or heavy per-frame math, convert just those subsystems. Mid-size studios running live multiplayer games should note that server-side DOTS adoption pairs well with deterministic fixed-tick simulation, but adds operational complexity to your backend: replay validation, desync detection, and load testing become engineering tasks in their own right. Tooling that monitors tick stability and server fleet health earns its keep once you exceed a handful of servers — this is where ops platforms for indie and mid-size teams justify their subscription versus hand-rolled dashboards.
Cost-wise, Unity's runtime fee controversy of 2023 ended with the fee scrapped in September 2024, and Unity 6 Plus pricing settled at standard subscription tiers (roughly $2,200 per seat per year for Pro as of 2026), so DOTS itself carries no extra license cost. The investment is engineering time: expect 20–40% longer initial implementation for jobified systems versus naive MonoBehaviour versions, repaid at 5x–50x runtime speedup on the converted paths.
Verdict and Recommendations
DOTS dependency management is best understood as a build graph for your frame: declare readers and writers, chain handles deliberately, and reserve main-thread synchronization for genuine boundaries like UI and networking. Teams that internalize three rules — never Complete without measuring, combine independent schedules explicitly, and route structural changes through sorted EntityCommandBuffers — avoid the vast majority of DOTS pain. Teams that skip those rules tend to abandon DOTS midway and blame the technology. Treat the dependency graph as a first-class architectural artifact: review it in code reviews, visualize it with the Profiler's flow events, and document sync points in your systems architecture doc. Done well, a mid-size team can sustain 100,000+ actively simulated entities at 60 FPS on hardware as modest as a Steam Deck-class APU; done poorly, the same codebase crawls at 20 FPS with workers idle. The difference is almost entirely dependency discipline.