cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

SharedArrayBuffer & Atomics

Learn how SharedArrayBuffer and Atomics let workers share counters safely, coordinate waiting, and meet browser isolation rules.

By the end, you can
  • 01
    Share a buffer safelyExplain what workers share through a SharedArrayBuffer and why a typed array view is needed.
  • 02
    Choose atomic operationsUse load, store, add, sub, exchange, and compareExchange for small shared integer cells.
  • 03
    Coordinate agentsDistinguish blocking wait, notify, waitAsync, and the browser headers needed for shared memory.

One memory area, many workers

Shared memory means two or more JavaScript agents can read and write the same bytes. A page and a worker normally exchange copied messages. A shared buffer is different: each agent has its own JavaScript world, but chosen typed-array views point at one backing block of bytes.

This is an expert tool for a small coordination problem, not a way to share every object. You choose a compact layout, give each integer cell a job, and decide which contested updates need an atomic rule. Variables, objects, functions, and call stacks remain private to each agent.

Definition

Atomics is a namespace of operations that read or update one integer typed-array cell as one indivisible action. Another atomic operation cannot observe an unfinished half of that operation.

This lesson continues from How workers work and prepares for The memory model. First learn the small practical pieces; the next lesson explains the ordering guarantees underneath them.

SharedArrayBuffer

A SharedArrayBuffer is raw bytes that compatible agents can share. It is not an array of JavaScript objects. Put an Int32Array view over it when you need integer cells; index zero then names the first four-byte signed integer.

Add five to one integer cellPop out in the code editor (opens in a new tab)JavaScript
const counter = new Int32Array(new ArrayBuffer(4));Atomics.add(counter, 0, 5);console.log(Atomics.load(counter, 0));

Line 1 creates an ordinary integer cell at zero. Line 2 atomically adds five. Line 3 loads the final value and prints 5. This plain typed-array example runs without browser isolation, so it teaches the operation before shared browser memory enters the picture.

A shared buffer changes the storage, not the view syntax. Two views are not equal arrays with equal starting values. They are windows onto the exact same bytes, so a write through one is visible through the other.

Two views of one shared cellJavaScript
const bytes = new SharedArrayBuffer(4);const pageView = new Int32Array(bytes);const workerView = new Int32Array(bytes);pageView[0] = 3;workerView[0] = 8;console.log(pageView[0], workerView[0]);

Line 1 allocates shared bytes. Lines 2 and 3 make the two views. Line 4 writes three, line 5 overwrites it with eight through the second view, and line 6 prints 8 8. A page and worker would create the same kind of separate views. Run this in Node.js or a cross-origin-isolated page; the lesson editor does not provide that environment or define SharedArrayBuffer.

Copied values, transferred resources, and shared bytes
MechanismWhat crosses agentsWhat later writes mean
Normal messageA structured copy of valuesThe sender and receiver change separate copies.
TransferableOwnership of one transferable resourceThe sender normally loses access after transfer.
SharedArrayBufferOne backing block of bytesAll views observe writes to those bytes.
Normal variableOne agent's JavaScript valueIt remains private to that agent.
Create shared bytes only when isolation is availablePop out in the code editor (opens in a new tab)JavaScript
if (typeof SharedArrayBuffer === "function" && crossOriginIsolated) {  const counter = new Int32Array(new SharedArrayBuffer(4));  Atomics.add(counter, 0, 5);  console.log(Atomics.load(counter, 0));} else {  console.log("Shared memory needs cross-origin isolation");}

Line 1 feature-detects the constructor and browser isolation. Lines 2 and 3 create shared bytes and a view. An isolated browser prints 5; another browser prints the requirement instead of throwing.

Real-life analogyA shared whiteboard

A shared whiteboard lets everyone read the same number. If two people change it together, they need a rule for who updates it safely.

In real life: One whiteboard is in the room
In JavaScript: One SharedArrayBuffer holds bytes
In real life: Everyone sees the same number
In JavaScript: Views read the same integer cell
In real life: One marker changes the number
In JavaScript: An atomic operation changes a cell
In real life: Each person has a private notebook
In JavaScript: Each agent keeps private variables

Where the analogy stops: A whiteboard does not decide turn-taking. Your program still needs a protocol for contested changes.

Why ordinary updates can race

An increase looks like one source line, but its meaning is read, add, then write. Two workers can both read zero before either writes. Both calculate one, and the later write replaces the earlier one even though the team intended two increases.

This outcome is a lost update. It explains why a shared counter needs more than shared bytes. A real non-atomic run may choose a different schedule, so this lesson uses a deterministic model instead of waiting for a timing bug to appear.

A deterministic lost-update teaching modelPop out in the code editor (opens in a new tab)JavaScript
let count = 0;const firstRead = count;const secondRead = count;count = firstRead + 1;count = secondRead + 1;console.log(count);

Line 1 starts at zero. Lines 2 and 3 save zero for both simulated workers. Lines 4 and 5 both write one, so line 6 prints 1. The model chooses the dangerous both-read interleaving; it does not claim every race visibly loses an update.

Step through one lost-update interleaving
Step 0 of 6Ready
Your turn: follow the blue line

Replay an instrumented teaching model of a lost update. It is not a claim that every non-atomic run loses an update.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const firstRead = count;const secondRead = count;count = firstRead + 1;count = secondRead + 1;console.log(count);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Step through the replay and stop at each saved read and write. It is a replay of instrumented lesson code, not an engine debugger. Its value is making the stale second observation visible before it overwrites the first result.

Playground: choose an update interleaving
Non-atomic read, add, writeJavaScript
let count = 0;const firstRead = count;const secondRead = count;count = firstRead + 1;count = secondRead + 1;console.log(count);
Model resultfinal count 1
  1. first reads 0
  2. second reads 0
  3. first writes 1
  4. second writes 1
Atomic alternativeJavaScript
const count = new Int32Array(new ArrayBuffer(4));
Atomics.add(count, 0, 1);
Atomics.add(count, 0, 1);
console.log(Atomics.load(count, 0));
Try it yourself
Choose the order

This model ends at 1. Both workers used the same earlier read, so the second write overwrote the first.

This deterministic model shows one possible interleaving. It does not claim every non-atomic run loses an update.

Choose another ordering. If a worker finishes before the other reads, the final count is two. If both read first, both use stale zero and the final count is one. Only the schedule changes, so the reason for the difference stays clear.

Atomic operations

Use an atomic operation when agents contend for one integer cell. It completes as one indivisible action with respect to other atomic operations. It does not make an entire application thread-safe; you still define state meanings and the order agents use them.

Load one stored scorePop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));Atomics.store(score, 0, 3);console.log(Atomics.load(score, 0));

Line 1 creates one cell. Line 2 stores three. Line 3 atomically loads it and prints 3. Use load when the current value is the question.

Store a new scorePop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));console.log(Atomics.store(score, 0, 3));console.log(Atomics.load(score, 0));

Line 2 writes three and returns the stored value, so it prints 3. Line 3 confirms that value. Store changes state; it does not wake a waiting worker by itself.

Add points and read the new totalPop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));Atomics.store(score, 0, 3);console.log(Atomics.add(score, 0, 2));console.log(Atomics.load(score, 0));

Line 2 starts at three. Line 3 adds two but returns the old value, so it prints 3. Line 4 loads after the change and prints 5. That old-value return is important for counters and compare-and-swap loops.

Read, write, and change one scorePop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));Atomics.store(score, 0, 3);console.log(Atomics.load(score, 0));console.log(Atomics.add(score, 0, 2));console.log(Atomics.sub(score, 0, 1));console.log(Atomics.exchange(score, 0, 9));console.log(Atomics.compareExchange(score, 0, 9, 12));console.log(Atomics.load(score, 0));

Line 2 stores three and line 3 prints 3. The add and sub lines print old values 3 and 5. Exchange prints 4, compareExchange prints 9, and the final load prints 12.

Small atomic tools for one integer cell
OperationWhat it doesWhat it returns
loadRead one cell atomically.Returns the current value.
storeWrite one cell atomically.Returns the stored value.
add and subChange a cell by an amount.Return the old value.
exchangeReplace a cell unconditionally.Returns the old value.
compareExchangeReplace only when a value matches.Returns the old value either way.

These operations work on integer typed arrays, not arbitrary objects. Prefer messages for rich data and infrequent work. A shared cell is useful because its tiny contract is easy to document, inspect, and test.

Compare and swap

Compare-and-swap means “change it only if it still has the value I saw.” Atomics.compareExchange checks a cell and possibly replaces it in one atomic operation. It returns the old value whether the check succeeds or fails.

Replace only a matching scorePop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));Atomics.store(score, 0, 3);console.log(Atomics.compareExchange(score, 0, 3, 8));console.log(Atomics.load(score, 0));

Line 2 stores three. Line 3 sees expected three, returns old value 3, and replaces the cell with eight. Line 4 prints 8. The comparison and replacement are one operation, not a separate if check followed by a later write.

Step through compare-and-swap
Step 0 of 4Ready
Your turn: follow the blue line

Replay a real compare-and-swap call. The old value tells you whether the expected value matched.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
Atomics.store(cell, 0, 3);const old = Atomics.compareExchange(cell, 0, 3, 8);console.log(old, Atomics.load(cell, 0));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay shows the success path: old value three and current cell eight have different jobs. The returned old value lets a caller tell whether it must refresh its observation and retry.

A retry loop refreshes a stale observationPop out in the code editor (opens in a new tab)JavaScript
const score = new Int32Array(new ArrayBuffer(4));Atomics.store(score, 0, 3);let seen = Atomics.load(score, 0);while (Atomics.compareExchange(score, 0, seen, seen + 1) !== seen) {  seen = Atomics.load(score, 0);}console.log(Atomics.load(score, 0));

Line 3 reads a score. Line 4 tries to replace it with one more. If another worker changed the cell first, the returned value differs from seen, line 5 refreshes the value, and the loop retries. With no competing write here, line 7 prints 4.

Keep retry loops small and give them a clear progress story. They suit compact state transitions, not unbounded work. The next memory-model lesson explains the ordering rules that make this pattern reliable.

Wait, notify, and waitAsync

A counter update is immediate. Sometimes an agent should pause until another agent changes a shared flag. Waiting lets an allowed worker sleep while a cell has an expected value instead of repeatedly reading in a busy loop.

A worker waits, then another agent notifies itJavaScript
// worker.js: never call Atomics.wait on the browser main thread.const cells = new Int32Array(sharedBuffer);const result = Atomics.wait(cells, 0, 0);console.log(result); // "ok" after another agent calls notify // main thread or another workerAtomics.store(cells, 0, 1);Atomics.notify(cells, 0, 1);

This is a worker sketch and is not runnable in the page sandbox. Line 2 makes a shared integer view. Line 3 blocks an allowed worker while cell zero is zero. Lines 7 and 8 store the new state first and then notify a waiter, allowing wait to return ok.

A waiter gets not-equal when the cell already changed before it waited. It can get timed-out when given a timeout. Notify is not a write; it is a wake-up signal after a store.

Wait without blocking the main threadJavaScript
const cells = new Int32Array(sharedBuffer);const result = Atomics.waitAsync(cells, 0, 0);if (result.async) {  result.value.then((status) => console.log(status));} else {  console.log(result.value); // "not-equal" or "timed-out"}

Line 2 calls waitAsync, which returns an object rather than freezing the agent. When async is true, line 4 gets a promise and later prints ok. Otherwise line 7 immediately prints a status such as not-equal.

Three ways agents coordinate around a cell
ToolMeaningWhere it belongs
Atomics.waitBlocks while a cell has an expected value.Worker only in a browser.
Atomics.notifyWakes waiters for a cell.Returns how many woke.
Atomics.waitAsyncReturns a result whose value may be a promise.Does not block the main thread.

Think of this as a tiny state machine: zero means no job, one means job ready, and notify tells a sleeping worker to check again. Store state before notify. A wake-up is not proof that the state is still what the worker hoped for.

Cross-origin isolation

Browsers require cross-origin isolation before a page can create and send shared buffers. Shared memory and precise timers can support side-channel attacks such as Spectre, so the page and its embedded resources must opt into a stronger boundary.

Headers and a runtime checkHTTP
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp if (crossOriginIsolated) {  const bytes = new SharedArrayBuffer(16);  console.log(bytes.byteLength);}

Line 1 sets Cross-Origin-Opener-Policy, or COOP, to same-origin. Line 2 sets Cross-Origin-Embedder-Policy, or COEP, to require-corp; credentialless is another COEP option. Line 4 checks crossOriginIsolated.

COEP changes how third-party resources load, so it is an application security decision rather than a local worker setting. Audit scripts, images, frames, and API resources before enabling it. The browser security model lesson goes deeper on COOP and COEP.

A worker counter in practice

A practical design stays small. A worker pool receives jobs by messages, and each completed job atomically increases one shared integer counter. The page can read the counter without asking every worker for another status message.

Two workers finish two jobsJavaScript
const progress = new Int32Array(new SharedArrayBuffer(4)); function finishJob() {  Atomics.add(progress, 0, 1);} finishJob(); // worker one finishesfinishJob(); // worker two finishesconsole.log(Atomics.load(progress, 0));

Line 1 reserves one progress cell. Lines 3 through 5 define the worker action: add one when a job finishes. Lines 7 and 8 model two workers completing jobs, and line 9 prints 2. A real worker receives the buffer in its startup message. This synchronous model can run in Node.js. A real browser worker needs a cross-origin-isolated page, and Node workers need the buffer passed through worker_threads; this editor sandbox has no shared buffer.

Reserve cells by purpose and document who writes them. Cell zero can be completed jobs, cell one total jobs, and cell two a ready flag. Store a state before notifying a worker, and let normal messages carry names, errors, and large results.

What job does each shared-memory piece have?
  • SharedArrayBuffer
  • Atomics.add
  • Atomics.compareExchange
  • Atomics.wait
  • Atomics.notify
  • Cross-Origin-Embedder-Policy
Try it yourself
0 of 6 correct

Sort each card by whether it changes memory, coordinates agents, or configures browser access.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

In Node, two worker_threads workers can each add one thousand on one SharedArrayBuffer counter and finish at exactly 2000. The lesson test proves that stable fact, compare-and-swap values, and a wait/notify wake-up without relying on an unreliable non-atomic race.

Common misconceptions

Shared memory is deliberately narrow. Most mistakes come from treating it as ordinary object sharing or treating one safe cell operation as a complete application design. Keep the layout small enough that another developer can explain each index.

  • “SharedArrayBuffer shares all variables.” Only backing bytes are shared.
  • “Atomics makes every algorithm thread-safe.” State layout and protocol still need design.
  • “wait is await on the main thread.” wait blocks; waitAsync is non-blocking.
  • “notify changes a value.” Store state first, then notify.
  • “COOP and COEP are optional.” Browser shared memory needs isolation.

The safe default is still messages. Reach for shared memory after design and profiling point to a compact shared-state problem: a worker-pool counter, producer-ready flag, or WebAssembly memory bridge.

Practice exercises

Work from the visible cell value and protocol state. Each task asks about one operation or one browser rule, so trace the code before answering.

Exercise 1 · Warm-upPredict the tiny counter

Read the program and type its output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const counter = new Int32Array(new ArrayBuffer(4));
Atomics.add(counter, 0, 5);
console.log(Atomics.load(counter, 0));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a failed compare-and-swap

    Type the two values printed by this program.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cell = new Int32Array(new ArrayBuffer(4));
    Atomics.store(cell, 0, 4);
    console.log(Atomics.compareExchange(cell, 0, 3, 9));
    console.log(Atomics.load(cell, 0));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeSpot the race model

      Choose both workers reading first. What final count does the model show?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePlace blocking wait

        Where may a browser app use blocking Atomics.wait?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeEnable the security boundary

          Which two headers enable cross-origin isolation?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply it to a real worker pool

            Which primitive updates a shared job counter?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Separate shared bytes from private variables, changing a cell from waiting for a state change, and atomicity from a complete protocol.

              Shared memory and Atomics quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What does a SharedArrayBuffer share?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const counter = new Int32Array(new ArrayBuffer(4));
                Atomics.add(counter, 0, 5);
                console.log(Atomics.load(counter, 0));

                Choose an answer to see the explanation.

              3. Question 3 of 8What does Atomics.add return?

                Choose an answer to see the explanation.

              4. Question 4 of 8When does compare-and-swap write its replacement?

                Choose an answer to see the explanation.

              5. Question 5 of 8Why is blocking wait not for the browser main thread?

                Choose an answer to see the explanation.

              6. Question 6 of 8Which headers normally make a page cross-origin isolated?

                Choose an answer to see the explanation.

              7. Question 7 of 8A worker sends { score: 3 } with normal postMessage and later changes its local object. What does the receiver have?

                Choose an answer to see the explanation.

              8. Question 8 of 8A waiter sees a cell is already 1 when it expected 0. What can waitAsync report immediately?

                Choose an answer to see the explanation.

              Key takeaways

              • SharedArrayBuffer shares chosen bytes while variables and call stacks stay private.
              • Use integer views and Atomics for contested cells.
              • compareExchange changes only a value that still matches.
              • wait and notify coordinate workers; waitAsync keeps a main thread responsive.
              • Browser shared memory needs COOP, COEP, and crossOriginIsolated.

              Remember the one-liner.
              Shared memory shares bytes; Atomics gives contested integer cells a safe update rule.

              Coming next: The memory model, where data races, ordering, and the guarantees behind atomic operations become precise.

              CompleteFrontend Clear concepts. Working examples.