Under the Hood
Webgpu

The device, the adapter, and the queue

Every later WebGPU lesson assumes you already hold a device and a queue, so this lesson walks the handshake that produces them — feature-detecting navigator.gpu, requesting an adapter that represents one specific physical GPU, requesting a logical device from it, and arriving at the single queue through which all GPU work is submitted.

The device, the adapter, and the queue

The previous lesson ended on a promise: instead of a global context you mutate, WebGPU gives you an explicit device and a single queue. Every lesson after this one — buffers, pipelines, bind groups, command encoders — starts its code with the same assumption already satisfied: "you have a device and a device.queue." This lesson is where that assumption gets earned. Nothing here draws a triangle. It's entirely about the handshake that gets you from "a browser tab" to "a validated, logical connection to a real GPU," and it happens in four steps, each one narrowing further than the last.

Step 1: navigator.gpu, the entry point that might not exist

WebGPU support is still rolling out browser by browser, and it's gated behind a secure context (HTTPS, or localhost). That means the very first thing any WebGPU code has to do is check that the API exists at all, before touching it:

if (!navigator.gpu) {
  // WebGPU isn't available: unsupported browser, or an insecure context.
  // Fall back to WebGL, or show a message. Nothing past this point exists.
  throw new Error("WebGPU is not supported in this browser.");
}

navigator.gpu is a GPU object when the browser supports the API and the page is served securely; it's simply undefined otherwise. There's no exception to catch, no capability-detection dance beyond this one check — just an object that either exists or doesn't. This is different from WebGL, where canvas.getContext('webgl') almost always succeeds because WebGL has been universal for a decade. WebGPU is young enough that "does this even exist" is a real, first-line question, not a formality.

Step 2: requestAdapter() — one specific physical GPU

navigator.gpu itself doesn't do anything except hand out adapters. An adapter (GPUAdapter) represents one actual, physical GPU implementation available on the machine — not a connection you can use yet, just a description of what's there and a handle you can use to request one:

const adapter = await navigator.gpu.requestAdapter({
  powerPreference: "high-performance",
});

if (!adapter) {
  throw new Error("No suitable GPU adapter found.");
}

Notice the await. requestAdapter is asynchronous and returns a promise — the browser may need to enumerate hardware, negotiate with the OS's native driver layer, or even spin up a separate GPU process, none of which is instant. It can also resolve to null if no adapter satisfies the request, which is why the null-check matters just as much as the initial navigator.gpu check.

The powerPreference option matters most on laptops with two GPUs: an integrated one (shares memory with the CPU, sips battery) and a discrete one (its own memory, far more powerful, drains battery faster). Passing 'high-performance' asks the browser to prefer the discrete GPU; 'low-power' asks for the integrated one. It's a preference, not a guarantee — the browser and OS get the final say, and on a machine with only one GPU it does nothing at all.

Before you commit to this adapter, you can inspect what it actually supports:

console.log(adapter.features); // a GPUSupportedFeatures set — optional capabilities
console.log(adapter.limits);   // a GPUSupportedLimits object — maxima, e.g. maxBufferSize

adapter.features lists optional capabilities this specific GPU/driver combination supports beyond the guaranteed baseline (things like extended texture formats). adapter.limits gives you the actual maximums this hardware allows — maximum buffer size, maximum bind groups, maximum texture dimensions, and so on. This is the point where you find out what you're working with, before you've committed to anything. That inspect-before-commit ordering is deliberate: the adapter is cheap and disposable, so you can check its capabilities and simply request a different configuration, or bail out with a clear error, before creating anything more expensive.

Step 3: requestDevice() — your sandboxed logical connection

The adapter describes hardware; the device (GPUDevice) is what you actually use. Requesting one is where you commit to specific requirements, checked against what you just inspected:

const device = await adapter.requestDevice({
  requiredFeatures: [],       // any optional features from adapter.features you need
  requiredLimits: {},         // any limits stricter than the default you need to guarantee
});

This is also asynchronous — again, the browser is coordinating with a native driver to actually stand up a connection, which takes real, non-instant work. If you ask for a requiredFeatures entry the adapter didn't report, or a requiredLimits value the adapter can't satisfy, the promise rejects rather than silently giving you a device that quietly fails later.

The device is the object from which every GPU resource in the rest of this module gets created: device.createBuffer(...), device.createShaderModule(...), device.createRenderPipeline(...), device.createCommandEncoder(...). It's a logical connection — sandboxed from other pages and other WebGPU contexts on the same physical GPU, so one tab's misbehaving code can't corrupt another tab's GPU state. Everything you build in lessons 3 through 8 hangs off this one object.

Devices are not permanent. A device.lost promise resolves if the connection dies underneath you — a GPU driver crash, a driver update, the OS reclaiming the GPU from a backgrounded tab, even the user's GPU physically resetting. It's worth at least acknowledging in real code:

device.lost.then((info) => {
  console.error(`Device lost: ${info.message}`);
  // Real applications typically re-run the whole handshake here
  // to get a fresh device, rather than treating this as fatal.
});

This isn't a corner case to ignore — GPUs reset under memory pressure or driver updates more often than CPUs crash, and an application that doesn't handle device.lost just silently stops rendering with no explanation.

Step 4: device.queue — the one door all work walks through

The device itself doesn't execute anything. Every device carries exactly one GPUQueue, accessible as device.queue, and it is the single point through which all GPU work — every buffer write, every submitted batch of rendering or compute commands — reaches the hardware:

device.queue.writeBuffer(someBuffer, 0, someTypedArrayData);
device.queue.submit([commandBuffer]); // commandBuffer comes from a command encoder, lesson 7

Contrast this with WebGL's implicit global context: in WebGL, calling gl.bufferData or gl.drawArrays pokes the driver immediately, through whatever the currently-bound global state happens to be, with no single named channel and no batching. WebGPU inverts that — there is exactly one queue object, it's explicit, and everything that touches the GPU goes through it by name. writeBuffer and submit are the two calls you'll see constantly in every remaining lesson in this module; this is the first time you're meeting the object they're both methods on.

The whole chain, together

Put together as one runnable handshake:

async function getGPU() {
  if (!navigator.gpu) {
    throw new Error("WebGPU is not supported in this browser.");
  }

  const adapter = await navigator.gpu.requestAdapter({
    powerPreference: "high-performance",
  });
  if (!adapter) {
    throw new Error("No suitable GPU adapter found.");
  }

  const device = await adapter.requestDevice();

  device.lost.then((info) => {
    console.error(`Device lost: ${info.message}`);
  });

  return { adapter, device, queue: device.queue };
}

Four steps, two of them asynchronous, each one narrower than the last: an entry point that might not exist, an adapter describing one specific piece of hardware, a device that's your sandboxed logical connection to it, and a queue that's the one door all work walks through. Every object every later lesson uses gets created from device, and every byte of work reaches the GPU through device.queue.

Where this goes next

You now hold a device and a queue, but they're empty — nothing has been allocated on the GPU yet. Buffers and the explicit memory model is where you use device.createBuffer to actually claim GPU memory, and it leans on exactly the same philosophy this lesson demonstrated in miniature: declare what you need up front (requiredFeatures, requiredLimits), rather than letting the driver infer it as you go.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Why does WebGPU code need to feature-detect navigator.gpu, when WebGL code almost never checks whether getContext succeeds?
  2. What does an adapter actually represent, and why is checking adapter.features and adapter.limits before requesting a device useful?
  3. Explain what powerPreference does and why it only matters on some machines.
  4. Why are both requestAdapter and requestDevice asynchronous — what is the browser actually doing during that wait?
  5. What is the relationship between a device and its queue — can a device have more than one queue, and what goes through the queue that doesn't go anywhere else?
  6. Give two real-world causes of device loss, and explain why ignoring device.lost is a real gap rather than a theoretical one.
  7. Contrast 'all work goes through device.queue' with how WebGL dispatches a draw call. What is WebGPU trading away, and what does it get in return?