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 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); // 42Here, 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.
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)); // 1Atomics.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
- 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.
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
- 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.
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); // 2This 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.
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()); // 312. 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.