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.

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 or Core Idea

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.

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

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

Common Atomics methods

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.

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

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.

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.

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.

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

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.

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

Atomics.add(view, 0, 1);

Fix: Create the view from SharedArrayBuffer instead.

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.

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.

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.

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().

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.

// 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.

// 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.

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

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.

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

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

11. Practice Exercise

Create a shared turn counter for a simple worker pool:

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

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

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.