JavaScript Atomics API: Shared Memory, Atomic Operations, and Concurrency

The JavaScript Atomics API lets you safely read, write, and coordinate access to shared memory between workers. It is the low-level tool you use when multiple threads need to update the same data without race conditions.

Quick answer: Atomics provides special methods such as Atomics.load(), Atomics.store(), Atomics.add(), and Atomics.compareExchange() for shared typed arrays backed by SharedArrayBuffer. Use it when you need thread-safe coordination between workers, not for normal single-threaded JavaScript.

Difficulty: Advanced

You'll understand this better if you know: how arrays and typed arrays work, what workers are, and why shared data can create race conditions.

1. What Is the JavaScript Atomics API?

The Atomics API is a built-in set of methods for performing operations on shared typed array elements in a way that cannot be interrupted by another agent at the same memory location. In practice, it is used with SharedArrayBuffer and typed arrays like Int32Array or BigInt64Array.

  • It provides atomic read, write, add, exchange, and compare-and-swap operations.
  • It works on shared memory, usually between the main thread and workers.
  • It helps prevent race conditions when multiple agents access the same data.
  • It also supports waiting and notification for coordination with Atomics.wait() and Atomics.notify().

Atomic operations are not the same as ordinary array access. A normal write like arr[0] = 1 can be vulnerable to timing problems when multiple workers share the same memory. Atomic methods make the operation indivisible at the memory level.

2. Why the Atomics API Matters

JavaScript is often described as single-threaded, but modern runtimes can run work in parallel through workers and shared memory. Without atomic coordination, two workers can read the same value, both compute a new value, and one update can overwrite the other.

The Atomics API matters because it gives you the tools to build correct concurrent code when simple message passing is not enough. It is especially useful for counters, locks, semaphores, and producer-consumer queues.

You usually do not need Atomics for ordinary UI or server code. You need it when your program intentionally shares memory across concurrent agents and correctness depends on exact ordering.

3. Basic Syntax

Atomics methods operate on integer typed arrays backed by shared memory. The general pattern is to create a SharedArrayBuffer, wrap it in a typed array, and then call an Atomics method on that typed array.

Minimal setup

This example creates shared memory and performs an atomic write and read on the same slot.

JavaScript
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);

Atomics.store(view, 0, 42);
const value = Atomics.load(view, 0);

console.log(value); // 42
Output✓ Tested · Node 24
42

Here, Atomics.store() writes the value and Atomics.load() reads it back safely from shared memory.

Common Atomics methods

  • Atomics.load(typedArray, index) reads a value atomically.
  • Atomics.store(typedArray, index, value) writes a value atomically.
  • Atomics.add(typedArray, index, value) adds and returns the old value.
  • Atomics.sub(typedArray, index, value) subtracts and returns the old value.
  • Atomics.exchange(typedArray, index, value) replaces a value and returns the old one.
  • Atomics.compareExchange(typedArray, index, expected, replacement) updates only if the current value matches.

4. Step-by-Step Examples

Example 1: Atomic counter

A shared counter is one of the simplest real uses for Atomics. Each worker can increment the same slot safely, and every increment is preserved.

JavaScript
const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);

Atomics.store(counter, 0, 0);
const previous = Atomics.add(counter, 0, 1);

console.log(previous); // 0
console.log(Atomics.load(counter, 0)); // 1
Output✓ Tested · Node 24
0
1

Atomics.add() returns the old value, which is useful when you need unique sequence numbers or IDs.

Example 2: Compare and swap for a lock

Atomics.compareExchange() is the basis for many lock-like algorithms. It checks a value and replaces it only if the current value matches what you expect.

JavaScript
const buffer = new SharedArrayBuffer(4);
const state = new Int32Array(buffer);

Atomics.store(state, 0, 0); // 0 = unlocked, 1 = locked

function tryLock() {
  return Atomics.compareExchange(state, 0, 0, 1) === 0;
}

function unlock() {
  Atomics.store(state, 0, 0);
}

This pattern lets you implement mutual exclusion when multiple workers must not enter a critical section at the same time.

Example 3: Waiting for work

One worker can sleep until another worker changes a shared flag. This avoids busy-waiting and wasting CPU.

JavaScript
const buffer = new SharedArrayBuffer(4);
const flag = new Int32Array(buffer);

Atomics.store(flag, 0, 0);

function waitForReady() {
  Atomics.wait(flag, 0, 0);
  console.log("Ready!");
}

Another worker can set the flag and wake waiting agents with Atomics.notify(). In browsers, Atomics.wait() is not allowed on the main thread, so this is usually a worker-to-worker coordination pattern.

Example 4: Ticket assignment without duplicates

If several workers need unique ticket numbers, atomic increment gives each worker a different value even when requests happen simultaneously.

JavaScript
const buffer = new SharedArrayBuffer(4);
const tickets = new Int32Array(buffer);

Atomics.store(tickets, 0, 1000);

function nextTicket() {
  return Atomics.add(tickets, 0, 1) + 1;
}

The first call returns 1001, the next returns 1002, and so on, without collisions.

5. Practical Use Cases

  • Shared counters for analytics or job processing across workers.
  • Locks and semaphores for protecting critical sections.
  • Producer-consumer queues where workers signal when items are available.
  • Coordinating staged computation in browser workers or Node.js worker threads.
  • Building higher-level concurrency utilities such as barriers and condition variables.

In real projects, Atomics usually sits underneath a larger abstraction. You may not expose Atomics directly to application code, but it powers the synchronization layer.

6. Common Mistakes

Mistake 1: Using Atomics on a normal typed array

Atomics only works with shared typed arrays. A normal typed array looks similar, but it does not represent shared memory.

Problem: This code fails because the array is backed by a regular ArrayBuffer, not a SharedArrayBuffer.

JavaScript
const buffer = new ArrayBuffer(4);
const view = new Int32Array(buffer);

Atomics.add(view, 0, 1);

Fix: Create the view from SharedArrayBuffer instead.

JavaScript
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);

Atomics.add(view, 0, 1);

The corrected version works because Atomics requires shared memory.

Mistake 2: Using the wrong typed array type

Atomics methods work on integer typed arrays, not on every typed array. Beginners often try to use Float32Array because it seems natural for numeric data.

Problem: Floating-point views are not valid for Atomics, so the runtime rejects the operation.

JavaScript
const buffer = new SharedArrayBuffer(4);
const view = new Float32Array(buffer);

Atomics.store(view, 0, 1.5);

Fix: Use an integer view such as Int32Array or BigInt64Array, depending on the method you need.

JavaScript
const buffer = new SharedArrayBuffer(4);
const view = new Int32Array(buffer);

Atomics.store(view, 0, 1);

The fixed version works because Atomics is defined for integer shared typed arrays.

Mistake 3: Expecting ordinary read-modify-write code to be safe

A common concurrency bug is reading a value, changing it, and writing it back with ordinary code. That looks correct in a single thread, but it is not atomic when multiple workers can interleave operations.

Problem: Two workers can read the same old value, both add one, and one update can overwrite the other. This creates a lost update bug.

JavaScript
const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);

// Not safe under concurrent access
counter[0] = counter[0] + 1;

Fix: Use an atomic read-modify-write method such as Atomics.add().

JavaScript
const buffer = new SharedArrayBuffer(4);
const counter = new Int32Array(buffer);

Atomics.add(counter, 0, 1);

The corrected version works because the increment happens as one atomic operation.

7. Best Practices

Practice 1: Use Atomics for coordination, not general state management

Atomics is excellent for synchronization primitives, but it is too low-level for most application state. Keep your shared memory small and focused.

JavaScript
// Better: share a small flag or counter
const state = new Int32Array(new SharedArrayBuffer(4));
Atomics.store(state, 0, 1);

Small shared values are easier to reason about than trying to mirror entire objects in shared memory.

Practice 2: Prefer message passing when you do not need shared memory

In many worker designs, sending messages is simpler and safer than sharing memory. Use Atomics only when sharing data is truly necessary.

CSS
// Simpler pattern: send work instead of sharing a large structure
worker.postMessage({ task: "parse", id: 1 });

This reduces the risk of race conditions and often makes the code easier to debug.

Practice 3: Use compareExchange for state transitions

When a value needs to move between a few known states, compareExchange() is usually safer than separate read and write operations.

JavaScript
const buffer = new SharedArrayBuffer(4);
const status = new Int32Array(buffer);

function markRunning() {
  return Atomics.compareExchange(status, 0, 0, 1) === 0;
}

This pattern prevents two workers from both believing they successfully claimed the same task.

8. Limitations and Edge Cases

  • Atomics.wait() is not usable on the browser main thread, so waiting logic usually belongs in workers.
  • Shared memory support can be restricted by environment security settings, especially in browsers.
  • Atomics works on integer typed arrays, not on object arrays, regular arrays, or floating-point views.
  • Atomic operations are low-level and do not replace higher-level synchronization design, such as queues or task ownership.
  • Careless use can create deadlocks if a worker waits forever and nobody notifies it.

Warning: A waiting worker can block progress if the corresponding notify never happens. Always design a clear wake-up path and consider timeouts where appropriate.

9. Practical Mini Project

Let’s build a tiny shared counter that could be used by multiple workers to assign unique job numbers. The point is not the worker wiring itself, but the shared-memory pattern.

JavaScript
const buffer = new SharedArrayBuffer(4);
const jobs = new Int32Array(buffer);

Atomics.store(jobs, 0, 0);

function claimJobNumber() {
  return Atomics.add(jobs, 0, 1) + 1;
}

const first = claimJobNumber();
const second = claimJobNumber();

console.log(first);  // 1
console.log(second); // 2
Output✓ Tested · Node 24
1
2

This example shows a complete shared counter that produces unique values in sequence. In a real application, worker threads could all call the same function on the shared buffer and still receive non-overlapping numbers.

10. Key Points

  • Atomics is for shared memory concurrency, not for everyday single-threaded code.
  • It works with SharedArrayBuffer and integer typed arrays.
  • Atomics.add(), Atomics.store(), and Atomics.compareExchange() are the most common building blocks.
  • Atomics.wait() and Atomics.notify() support worker coordination.
  • Using Atomics correctly helps prevent race conditions and lost updates.

11. Practice Exercise

Create a shared turn counter for a simple worker pool:

  • Initialize a shared integer to 0.
  • Write a function that atomically increments the value and returns the new turn number.
  • Call the function three times and print the results.
  • Expected output: 1, 2, 3.

Hint: Use Atomics.add(), and remember that it returns the old value.

Show solution
JavaScript
const buffer = new SharedArrayBuffer(4);
const turn = new Int32Array(buffer);

Atomics.store(turn, 0, 0);

function nextTurn() {
  return Atomics.add(turn, 0, 1) + 1;
}

console.log(nextTurn()); // 1
console.log(nextTurn()); // 2
console.log(nextTurn()); // 3
Output✓ Tested · Node 24
1
2
3

12. Final Summary

The JavaScript Atomics API is the foundation for safe shared-memory coordination in concurrent JavaScript. It gives you atomic reads, writes, arithmetic, comparison, waiting, and notification so multiple workers can operate on the same data without corrupting it.

Most developers will use Atomics only in specialized concurrency code, but when they do, correctness depends on using the right shared typed array, choosing the right atomic primitive, and avoiding ordinary read-modify-write patterns. If you are building worker-based synchronization, next learn more about SharedArrayBuffer, worker threads, and concurrency patterns such as locks and queues.