-
Notifications
You must be signed in to change notification settings - Fork 0
06 — Quality of Service
Not every order matters equally right now.
A customer is standing at the counter, staring at the chef, waiting on the one dish left to finish. That order gets handled immediately — nothing else is more urgent.
A different customer took a buzzer and sat down, but they're actively watching the door for their food — they're waiting, just not standing right there. That order comes right after.
Meanwhile, there's prep work sitting in a bin — vegetables to chop, stock to reduce — for orders that won't be needed for a while yet. It can wait a bit without anyone noticing.
And at the very bottom, there's the end-of-day cleanup — wiping counters, restocking shelves. Nobody's waiting on it at all. It gets done whenever a chef 🧑🍳 has a spare moment, and not a second sooner.
- Customer staring at the chef, order finishing right now →
.userInteractive - Customer waiting on an order they're actively expecting →
.userInitiated - Prep work for later, can wait a bit →
.utility - End-of-day cleanup, whenever there's spare capacity →
.background
👉 It's the same kitchen, the same chefs, the same order line. What changes is how urgently each order gets treated.
GCD calls this urgency level Quality of Service, or QoS. Every block of work you submit carries a QoS, and it tells the system how aggressively to schedule it — how much CPU time it gets, and how quickly it gets a chef.
There are five QoS classes, from most to least urgent:
-
.userInteractive— work tied directly to the UI right now: animating, responding to a tap. It needs to happen almost instantly, or the app feels broken. -
.userInitiated— work the user is actively waiting on, just one step removed from the UI: opening a file they just tapped, loading a screen they just navigated to. -
.default— the QoS used when you don't specify one. It sits between.userInitiatedand.utility— a reasonable middle ground for work with no strong urgency signal either way. -
.utility— longer-running work the user asked for but isn't actively staring at: downloading a file, importing data. Progress may be visible, but nobody's frozen waiting on it. -
.background— work the user doesn't know is happening at all: prefetching, cleanup, indexing. Give it whatever's left over.
DispatchQueue.global(qos: .userInitiated).async {
// user is actively waiting on this
}
DispatchQueue.global(qos: .background).async {
// do this whenever, nobody's waiting
}👉 Higher QoS doesn't just run "sooner" in some vague sense — it gets real priority: more CPU time, and first access to chefs when the pool is busy. Lower QoS work can get starved for a while if the system is under load and higher-priority work keeps arriving.
PRIORITY LADDER — higher rungs get scheduled first, get more CPU 🪜
.userInteractive ██████████ "the customer is staring at me right now"
.userInitiated ████████ "the customer is actively waiting"
.default ██████ "no urgency specified — middle of the pack"
.utility ████ "prep work, can wait a bit"
.background ██ "whenever, nobody's watching"
More CPU time + earlier chef access ⬆
Less CPU time + waits for spare capacity ⬇
DispatchQueue.global(qos: .userInitiated).async {
// user is actively waiting on this
}
DispatchQueue.global(qos: .background).async {
// do this whenever, nobody's waiting
}👉 Same .global queue mechanism from Chapter 5 — the only thing changing here is the qos: you ask for. That single argument tells GCD how to compete this block against everything else in the shared chef pool.
Swift Concurrency doesn't reinvent priority — it exposes the same underlying mechanism through Task:
Task(priority: .high) {
// roughly maps to .userInitiated
}
Task(priority: .background) {
// roughly maps to .background
}-
Taskpriorities (.high,.medium,.low,.background, and a couple of finer-grained ones) sit on top of the exact same QoS system GCD has always used. - The names changed, and the API is nicer to write, but the scheduler underneath — how much CPU time and thread access a unit of work gets — is the one you already learned in this chapter.
👉 If you understand GCD's QoS, you already understand Task priority. It's the same ladder, relabeled.
QoS is how urgently the kitchen treats an order.
The priority ladder makes it sound like a strict, guaranteed ordering — .userInteractive always beats .background, full stop. It's not quite that clean.
- QoS is a hint to the system, not a contract. The OS weighs it against other signals — overall system load, thermal state, battery level — before deciding what actually runs next.
- It doesn't guarantee exact scheduling order between two blocks of the same QoS, and it doesn't guarantee a lower-QoS block never runs before a higher-QoS one submitted moments later — just that, on average and under pressure, higher QoS wins access to chefs and CPU time.
- Requesting a QoS you don't need has real cost: marking everything
.userInteractivedoesn't make your app faster, it just starves everything else and can drain battery faster for no benefit.
So "the priority ladder" captures the intent — who gets treated more urgently — but not a hard, guaranteed schedule.
You're writing code to clear out an old image cache on disk, and no part of the UI is waiting on it. Which QoS level fits — and what would go wrong if you picked .userInteractive instead?