Skip to content

06 — Quality of Service

rebeloper edited this page Jul 12, 2026 · 1 revision

🍽️ Intuition

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.


🧠 What It Means In Swift

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 .userInitiated and .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.


📊 ASCII Diagram

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 ⬇

💻 Tiny Swift Example

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.


🔄 vs. Swift Concurrency

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
}
  • Task priorities (.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.


🧸 Memory Sentence

QoS is how urgently the kitchen treats an order.


⚠️ Analogy Limit

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 .userInteractive doesn'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.


✅ Check Your Understanding

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?

Clone this wiki locally