OperationQueue is a high-level task queue in iOS and macOS built on top of Grand Central Dispatch. According to Apple Developer Documentation, 2026, OperationQueue manages instances of Operation — objects that encapsulate a unit of work. Unlike DispatchQueue, OperationQueue supports dependencies between operations, priorities, KVO observation, and cancellation of running tasks. OperationQueue automatically manages a thread pool, distributing operations across available system resources.
Key Takeaways
OperationQueue is a class from the Foundation framework that manages the execution of Operation objects. Unlike DispatchQueue, OperationQueue does not require explicitly specifying serial or concurrent mode — the number of simultaneously executing operations is controlled by the maxConcurrentOperationCount property. A value of 1 makes the queue sequential, any other value makes it concurrent.
Operation is an abstract class representing a unit of work. Each operation has a state: ready, executing, finished, or cancelled. The states are KVO (Key-Value Observing) compatible, allowing you to react to changes — for example, updating the UI when an operation completes. Operation automatically manages the isExecuting and isFinished flags.
According to Apple (2026), OperationQueue uses GCD under the hood but adds functionality not available in DispatchQueue: dependencies, priorities, and cancellation of operations. If the app enters the background, OperationQueue suspends execution and resumes when it returns. OperationQueue also automatically considers the number of CPU cores and selects the optimal number of threads.
Each operation goes through four states: pending, ready, executing, and finished. The cancelled state can occur at any stage before completion. State transitions are tracked via KVO — this is the foundation for reactive UI updates. OperationQueue automatically removes completed operations from the queue and notifies dependent operations that their prerequisite has been fulfilled, triggering their execution.
Operation is an abstract class that requires overriding the main() or start() method. The task code is placed in the main() method, and the isExecuting and isFinished states are managed automatically. For asynchronous operations, you need to override start() and manually manage the state flags.
BlockOperation is a concrete implementation of Operation that executes one or more code blocks. BlockOperation becomes concurrent if you add multiple blocks via addExecutionBlock(). The operation completes only after all added blocks have finished. BlockOperation is a convenient alternative for simple tasks without inheritance.
| Characteristic | Operation | BlockOperation |
|---|---|---|
| Class Type | Abstract | Concrete |
| Inheritance | Required | Not required |
| Asynchronicity | Manual KVO management | Automatic |
| Code Blocks | One in main() | One or many |
| Usage | Complex tasks with state | Simple one-time tasks |
| Suitable for | Dependencies, cancellation, progress | Quick blocks, completion |
| Memory | Higher due to KVO and state | Minimal, lightweight |
To create a custom operation, subclass Operation and override main(). Inside, check the isCancelled flag before expensive operations to ensure fast cancellation. This is critical for downloading large files or batch data processing. Choosing between Operation and BlockOperation depends on the task complexity: for simple one-time actions, BlockOperation is sufficient; for reusable logic with state, subclass Operation.
Dependencies are the key advantage of OperationQueue over DispatchQueue. The addDependency(_:) method specifies that operation B executes only after operation A completes. Dependencies form a directed acyclic graph (DAG): if a cyclic dependency is added, the queue ignores it and the operations do not start.
Priority of an operation is set via the queuePriority property with values: .veryLow, .low, .normal, .high, .veryHigh. Priority affects the launch order among ready operations but does not override dependencies. OperationQueue first resolves dependencies, then applies priority among available operations.
A typical scenario is loading profile data: first load the user, then based on their id load friends and posts. Setting a dependency between loading the user and loading friends guarantees correct order without nested completion handlers.
The maxConcurrentOperationCount property limits the number of concurrently executing operations. A value of 1 creates a sequential queue, the default value (NSOperationQueueDefaultMaxConcurrentOperationCount) is system-optimal, depending on the current device load. Proper configuration of this parameter prevents excessive resource consumption: for image loading, 4–6 concurrent operations are sufficient; for CPU-intensive tasks, use the number of CPU cores.
Choosing between OperationQueue and DispatchQueue depends on task complexity. DispatchQueue is a lightweight tool for simple async calls. OperationQueue is a heavier solution for complex scenarios with many interrelated tasks. Apple recommends starting with DispatchQueue and switching to OperationQueue only when dependencies or cancellation are needed. For most iOS projects, a combination of both tools provides an optimal balance of performance and flexibility.
According to Ray Wenderlich (2025), in large iOS projects, OperationQueue is used for content loading with progress and cancellation, while DispatchQueue is used for all other async operations. The ratio is approximately 20 to 80 in favor of DispatchQueue.
Let us look at three examples: a simple BlockOperation, a custom Operation with dependencies, and a cancellable operation for data loading.
The simplest case — execute a block on OperationQueue and handle the result via completionBlock. Every Operation has a built-in completionBlock property, called after main() finishes.
let queue = OperationQueue()
let operation = BlockOperation()
operation.addExecutionBlock {
let data = NetworkService.fetchData()
OperationQueue.main.addOperation {
self.updateUI(data)
}
}
queue.addOperation(operation)
Dependency ensures that parseOperation starts only after downloadOperation completes. This eliminates the need for nested callbacks.
let download = BlockOperation { self.downloadJSON() }
let parse = BlockOperation { self.parseJSON() }
parse.addDependency(download)
let queue = OperationQueue()
queue.addOperations([download, parse], waitUntilFinished: false)
Override main() with periodic isCancelled checks. This allows the operation to stop immediately upon cancellation, without waiting for an expensive operation to complete.
class ImageLoadOperation: Operation {
override func main() {
guard !self.isCancelled else { return }
let image = self.downloadImage()
guard !self.isCancelled else { return }
OperationQueue.main.addOperation { self.display(image) }
}
}
Cancellation of an operation sets the isCancelled flag to true, but does not stop an already running main() method. The code inside main() must check isCancelled by itself and terminate when necessary. This is Apple's architectural decision — allowing the developer to properly release resources upon cancellation.
KVO observation of isFinished and isExecuting properties allows reacting to operation completion without explicit callbacks. OperationQueue automatically removes completed operations from the queue, but they remain in memory as long as there are strong references to them. KVO is the foundation for integrating OperationQueue with reactive frameworks like RxSwift or Combine.
Subscribing to isCancelled via KVO allows updating the UI upon operation cancellation — for example, showing a placeholder instead of a cancelled download. The isCancelled property is KVO-compatible, making it convenient for reactive pipelines.
Do not create large numbers of operations — each Operation is a separate object in memory. If a task is short and does not require dependencies, use DispatchQueue directly. OperationQueue is justified for complex scenarios with explicit dependencies, cancellation, and progress monitoring.
Check isCancelled before expensive operations inside the main() method. In cases of file downloading or image processing, checking after each significant step ensures fast response to cancellation. Use if isCancelled { return } at the start of main() and after each major operation.
Manage completionBlock properly. The completionBlock property of an operation is called after main() finishes, even if the operation was cancelled. Check isCancelled inside completionBlock to avoid updating the UI with erroneous data. OperationQueue.main is a thread-safe queue for UI operations, similar to DispatchQueue.main.
Avoid cyclic dependencies — they cause none of the operations in the cycle to ever start. OperationQueue does not detect cycles automatically: if A depends on B and B depends on A, both will remain in the ready state forever. Plan the dependency graph in advance.
Frequently Asked Questions
OperationQueue is built on top of GCD and adds dependencies, priorities, KVO, and operation cancellation. DispatchQueue is a lighter tool for simple async tasks without these capabilities.
Set the maxConcurrentOperationCount property to 1. This turns OperationQueue into a sequential queue while retaining all advantages — dependencies, priorities, and cancellation.
The cancel() method sets the isCancelled flag but does not stop the executing main() method. The operation code must check isCancelled by itself and terminate. Cancellation only works for pending and ready operations.
You should subclass Operation when you need state management, asynchronicity, or logic reuse. BlockOperation is suitable for simple one-time tasks without inheritance.
No, unless you call waitUntilFinished with the parameter true on the main thread. Operations execute on background threads by default, and results are returned via OperationQueue.main.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also