cd /news/developer-tools/javascript-atomics-and-sharedarraybu… · home topics developer-tools article
[ARTICLE · art-58079] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

JavaScript Atomics and SharedArrayBuffer in 2026: Practical Patterns for Cross-Worker State

A developer explores practical patterns for using JavaScript Atomics and SharedArrayBuffer in 2026 to enable efficient cross-worker state sharing. The post demonstrates how shared memory eliminates serialization overhead, reducing frame copy times from 15-20ms to under 2ms for real-time workloads like video processing. It covers atomic operations for lock-free coordination and the security requirements of cross-origin isolation.

read18 min views1 publishedJul 13, 2026

This article was written with the assistance of AI, under human supervision and review.

Most cross-worker communication problems stem from treating workers as isolated processes when the workload demands shared state. Teams reach for postMessage

by default, serialize multi-megabyte data structures on every frame, and watch their real-time audio pipelines stutter under 100ms message latency. The browser gives developers true shared memory through SharedArrayBuffer

, but production codebases rarely exploit it because the API surface feels foreign and the security requirements seem burdensome.

The failure mode here is subtle but expensive. A video processing pipeline that bounces 1920×1080 frames through postMessage

spends 15-20ms per transfer just copying pixels. That overhead compounds across worker boundaries until the entire system misses its 16.67ms budget. Meanwhile, a SharedArrayBuffer

-backed ring buffer eliminates the copy entirely and keeps the same workload under 2ms.

The correct approach places pixel data in shared memory once, then coordinates access with atomic operations. Workers read and write the same underlying bytes without serialization. The synchronization primitives—Atomics.wait

, Atomics.notify

, compare-and-swap—replace message passing with lock-free coordination that runs in microseconds instead of milliseconds.

This distinction is critical because the web platform now ships SharedArrayBuffer

with reliable cross-origin isolation in every major browser. The security requirements that blocked adoption in 2018 are solved. Production teams that master these patterns unlock performance headroom that message passing cannot match.

SharedArrayBuffer

eliminates serialization overhead by giving workers direct access to the same memory, turning 15ms postMessage

copies into microsecond atomic operations for real-time workloads.Atomics.compareExchange

and Atomics.wait/notify

provide lock-free coordination primitives that replace mutex-heavy approaches, enabling ring buffers and work queues without blocking.SharedArrayBuffer

since 2020, but the security model is stable and shipping in all modern browsers as of 2026.postMessage

or Transferable

for simplicity.SharedArrayBuffer

allocates a contiguous block of memory that multiple workers can map into their address spaces simultaneously. Unlike ArrayBuffer

, which each worker owns exclusively, a SharedArrayBuffer

instance lives until all references drop. This shared ownership means concurrent reads and writes from different threads can observe each other's mutations without explicit synchronization—unless developers use atomic operations to enforce ordering.

The memory model follows sequential consistency for atomic operations and relaxed ordering for plain reads and writes. In other words, Atomics.load

and Atomics.store

guarantee that all workers see updates in the same order, while non-atomic accesses can reorder freely. This matters because a worker writing buffer[0] = 1; buffer[1] = 2;

might let another worker observe buffer[1] === 2

before buffer[0] === 1

due to CPU-level reordering. Atomic operations prevent this surprise.

Cross-origin isolation became mandatory for SharedArrayBuffer

after the Spectre disclosure in 2018. Browsers require two response headers: Cross-Origin-Opener-Policy: same-origin

and Cross-Origin-Embedder-Policy: require-corp

. These headers ensure that the page cannot load cross-origin content without explicit consent, closing the timing side-channel that Spectre exploits. As of 2026, every major browser enforces this requirement consistently.

Setting up shared memory in practice looks like this:

// main.ts — Create shared buffer and pass to workers
const sharedBuffer = new SharedArrayBuffer(1024 * 1024); // 1MB
const view = new Uint32Array(sharedBuffer);

const worker1 = new Worker('worker.js');
const worker2 = new Worker('worker.js');

worker1.postMessage({ sharedBuffer }, []);
worker2.postMessage({ sharedBuffer }, []);

// worker.js — Both workers receive the same buffer
self.addEventListener('message', (event) => {
  const { sharedBuffer } = event.data;
  const view = new Uint32Array(sharedBuffer);

  // Both workers can now read/write the same memory
  Atomics.store(view, 0, 42);
  console.log(Atomics.load(view, 0)); // 42 from either worker
});

The implication here is that developers control when to share and when to transfer. A SharedArrayBuffer

in the transfer list does nothing—it stays shared regardless. A regular ArrayBuffer

in the transfer list moves ownership. This asymmetry lets teams mix both models: transfer large immutable blobs (images, video frames) and share small mutable state (cursors, flags, counters).

The security model imposes real deployment constraints. Any page serving third-party widgets or embedded iframes cannot use SharedArrayBuffer

unless those resources opt in with CORP headers. For most applications, this means consolidating critical workers under the same origin and serving analytics or ads through separate, isolated contexts.

Atomics.load

and Atomics.store

guarantee sequential consistency—every worker sees updates in a single global order. These operations prevent the CPU from reordering memory accesses across atomic boundaries, which matters when implementing lock-free algorithms. A simple counter incremented with Atomics.add(view, index, 1)

never loses updates even when ten workers increment concurrently.

The operation set includes arithmetic (add

, sub

, and

, or

, xor

), compare-and-swap (compareExchange

), and synchronization primitives (wait

, notify

). Compare-and-swap is the foundation for lock-free structures: it reads a value, compares it to an expected value, and swaps in a new value only if the comparison matches—all in one atomic step. This enables patterns like lock-free stacks and queues.

Atomics.wait

blocks the calling worker until another worker calls Atomics.notify

on the same memory location, or until a timeout expires. This primitive replaces busy-waiting spin loops with efficient parking. A worker waiting for data can suspend immediately instead of burning CPU cycles checking a flag. The implication here is that wait

only works in workers—the main thread cannot block because that would freeze the UI.

// Producer worker — Writes data and signals consumers
function produceData(sharedView: Int32Array, dataIndex: number, flagIndex: number) {
  // Write actual data
  Atomics.store(sharedView, dataIndex, computeExpensiveValue());

  // Set ready flag
  Atomics.store(sharedView, flagIndex, 1);

  // Wake one waiting consumer
  Atomics.notify(sharedView, flagIndex, 1);
}

// Consumer worker — Waits for data to be ready
function consumeData(sharedView: Int32Array, dataIndex: number, flagIndex: number) {
  // Wait until flag becomes 1 (timeout after 1000ms)
  const result = Atomics.wait(sharedView, flagIndex, 0, 1000);

  if (result === 'ok') {
    const data = Atomics.load(sharedView, dataIndex);
    processData(data);

    // Reset flag for next round
    Atomics.store(sharedView, flagIndex, 0);
  } else if (result === 'timed-out') {
    console.warn('Producer did not signal within timeout');
  }
}

The wait

return value encodes three states: 'ok'

(woken by notify), 'not-equal'

(value changed before wait started), or 'timed-out'

. This matters because a producer might signal before the consumer calls wait

, causing a missed wakeup. Robust patterns check the flag value first, then only wait if the flag still indicates "not ready."

Compare-and-swap enables ownership transfer without locks. A task queue can use CAS to claim work items:

// Lock-free task claiming
function claimTask(sharedView: Int32Array, taskIndex: number): boolean {
  const UNCLAIMED = 0;
  const CLAIMED = 1;

  // Try to swap UNCLAIMED -> CLAIMED atomically
  const previous = Atomics.compareExchange(sharedView, taskIndex, UNCLAIMED, CLAIMED);

  // If previous was UNCLAIMED, we won the race
  return previous === UNCLAIMED;
}

// Worker loop
while (true) {
  for (let i = 0; i < taskCount; i++) {
    if (claimTask(sharedTasks, i)) {
      executeTask(i);
      Atomics.store(sharedTasks, i, 0); // Release back to pool
    }
  }
}

This distinction is critical because CAS-based algorithms scale better than mutex-based ones under contention. When ten workers compete for tasks, CAS lets them retry instantly on failure instead of blocking in a queue. The tradeoff is complexity: CAS loops require careful handling of the ABA problem (a value changes from A to B and back to A, fooling CAS into thinking nothing changed).

Real-time audio processing demands predictable latency under 5ms. A ring buffer backed by SharedArrayBuffer

lets an audio worklet write samples directly into shared memory while a processing worker consumes them without blocking. The pattern uses two atomic indices—read and write—to coordinate without locks.

// Shared ring buffer structure
interface RingBufferState {
  buffer: Float32Array;    // Audio samples
  writeIndex: Int32Array;  // [writePos, readPos]
  capacity: number;
}

class LockFreeRingBuffer {
  private buffer: Float32Array;
  private indices: Int32Array;
  private capacity: number;

  constructor(sharedBuffer: SharedArrayBuffer, sampleCount: number) {
    // First 8 bytes for indices, rest for audio samples
    this.indices = new Int32Array(sharedBuffer, 0, 2);
    this.buffer = new Float32Array(
      sharedBuffer,
      8,
      sampleCount
    );
    this.capacity = sampleCount;

    Atomics.store(this.indices, 0, 0); // writeIndex
    Atomics.store(this.indices, 1, 0); // readIndex
  }

  write(samples: Float32Array): boolean {
    const writePos = Atomics.load(this.indices, 0);
    const readPos = Atomics.load(this.indices, 1);

    // Calculate available space
    const available = (readPos - writePos - 1 + this.capacity) % this.capacity;

    if (available < samples.length) {
      return false; // Buffer full, drop samples
    }

    // Write samples in two chunks if wrapping
    const firstChunkSize = Math.min(
      samples.length,
      this.capacity - writePos
    );

    this.buffer.set(
      samples.subarray(0, firstChunkSize),
      writePos
    );

    if (firstChunkSize < samples.length) {
      this.buffer.set(
        samples.subarray(firstChunkSize),
        0
      );
    }

    // Advance write index atomically
    const newWrite = (writePos + samples.length) % this.capacity;
    Atomics.store(this.indices, 0, newWrite);

    return true;
  }

  read(output: Float32Array): number {
    const writePos = Atomics.load(this.indices, 0);
    const readPos = Atomics.load(this.indices, 1);

    // Calculate available samples
    const available = (writePos - readPos + this.capacity) % this.capacity;
    const toRead = Math.min(available, output.length);

    if (toRead === 0) {
      return 0; // Buffer empty
    }

    // Read samples in two chunks if wrapping
    const firstChunkSize = Math.min(
      toRead,
      this.capacity - readPos
    );

    output.set(
      this.buffer.subarray(readPos, readPos + firstChunkSize),
      0
    );

    if (firstChunkSize < toRead) {
      output.set(
        this.buffer.subarray(0, toRead - firstChunkSize),
        firstChunkSize
      );
    }

    // Advance read index atomically
    const newRead = (readPos + toRead) % this.capacity;
    Atomics.store(this.indices, 1, newRead);

    return toRead;
  }
}

// Audio worklet (producer)
class SharedBufferProcessor extends AudioWorkletProcessor {
  private ringBuffer: LockFreeRingBuffer;

  process(inputs: Float32Array[][], outputs: Float32Array[][]) {
    const input = inputs[0][0]; // Mono channel

    if (!this.ringBuffer.write(input)) {
      console.warn('Ring buffer overflow, dropping samples');
    }

    return true;
  }
}

// Processing worker (consumer)
const processingBuffer = new Float32Array(128);

function processAudioLoop(ringBuffer: LockFreeRingBuffer) {
  const samplesRead = ringBuffer.read(processingBuffer);

  if (samplesRead > 0) {
    // Apply effects, analysis, etc.
    applyReverb(processingBuffer.subarray(0, samplesRead));
  }

  // Loop without blocking
  setTimeout(() => processAudioLoop(ringBuffer), 1);
}

The ring buffer eliminates serialization overhead entirely. An audio worklet running at 48kHz generates 128 samples every 2.67ms. Copying those samples through postMessage

adds 0.5-1ms of latency per message. With shared memory, the write operation completes in under 10 microseconds—two orders of magnitude faster.

The failure mode here is buffer overflow when the producer outpaces the consumer. The pattern handles this gracefully by dropping samples instead of blocking the audio thread. Production systems monitor the drop rate and adjust buffer size dynamically: a sustained 5% drop rate triggers a capacity increase from 4096 to 8192 samples.

This matters because real-time audio is the canonical use case for shared memory. Message passing fundamentally cannot meet the latency budget. Teams building DAWs, live streaming encoders, or voice chat systems reach for SharedArrayBuffer

first and accept the complexity trade.

A worker pool pattern distributes tasks across multiple workers without a central coordinator. Tasks live in a shared queue, and workers claim them with compare-and-swap. This eliminates the bottleneck of routing work through the main thread and scales linearly with worker count.

// Shared task queue structure
const TASK_UNCLAIMED = 0;
const TASK_CLAIMED = 1;
const TASK_COMPLETE = 2;

interface TaskQueueLayout {
  // Task states: [state0, state1, ..., stateN]
  states: Int32Array;
  // Task payloads: [task0_data, task1_data, ...]
  payloads: Float64Array;
  taskCount: number;
}

class SharedTaskQueue {
  private states: Int32Array;
  private payloads: Float64Array;
  private taskCount: number;

  constructor(sharedBuffer: SharedArrayBuffer, taskCount: number) {
    this.taskCount = taskCount;
    this.states = new Int32Array(sharedBuffer, 0, taskCount);
    this.payloads = new Float64Array(
      sharedBuffer,
      taskCount * 4, // After states
      taskCount
    );
  }

  enqueueTask(taskId: number, payload: number): void {
    Atomics.store(this.payloads, taskId, payload);
    Atomics.store(this.states, taskId, TASK_UNCLAIMED);
  }

  claimTask(): number | null {
    // Linear scan for unclaimed task
    for (let i = 0; i < this.taskCount; i++) {
      const previous = Atomics.compareExchange(
        this.states,
        i,
        TASK_UNCLAIMED,
        TASK_CLAIMED
      );

      if (previous === TASK_UNCLAIMED) {
        return i; // Successfully claimed
      }
    }

    return null; // No tasks available
  }

  getTaskPayload(taskId: number): number {
    return Atomics.load(this.payloads, taskId);
  }

  completeTask(taskId: number, result: number): void {
    Atomics.store(this.payloads, taskId, result);
    Atomics.store(this.states, taskId, TASK_COMPLETE);
  }

  waitForCompletion(timeout: number = 5000): boolean {
    const start = Date.now();

    while (Date.now() - start < timeout) {
      let allComplete = true;

      for (let i = 0; i < this.taskCount; i++) {
        const state = Atomics.load(this.states, i);
        if (state !== TASK_COMPLETE) {
          allComplete = false;
          break;
        }
      }

      if (allComplete) return true;

      // Yield to avoid busy-wait
      Atomics.wait(this.states, 0, TASK_COMPLETE, 10);
    }

    return false;
  }
}

// Worker code
function workerLoop(queue: SharedTaskQueue) {
  const taskId = queue.claimTask();

  if (taskId !== null) {
    const input = queue.getTaskPayload(taskId);
    const result = performComputation(input);
    queue.completeTask(taskId, result);
  } else {
    // No work available, yield briefly
    setTimeout(() => workerLoop(queue), 5);
    return;
  }

  // Continue processing
  setTimeout(() => workerLoop(queue), 0);
}

// Main thread usage
const bufferSize = 1024 * 1024; // 1MB
const taskCount = 1000;
const sharedBuffer = new SharedArrayBuffer(bufferSize);
const queue = new SharedTaskQueue(sharedBuffer, taskCount);

// Spawn workers
const workers = Array.from({ length: 8 }, () => {
  const worker = new Worker('worker.js');
  worker.postMessage({ sharedBuffer, taskCount });
  return worker;
});

// Enqueue tasks
for (let i = 0; i < taskCount; i++) {
  queue.enqueueTask(i, Math.random() * 1000);
}

// Wait for completion
if (queue.waitForCompletion(10000)) {
  console.log('All tasks complete');

  // Collect results
  const results = Array.from({ length: taskCount }, (_, i) =>
    queue.getTaskPayload(i)
  );
}

This pattern shines when task execution time varies widely. A message-passing coordinator must track which workers are idle and route tasks accordingly. The shared queue eliminates that bookkeeping—workers self-schedule by claiming whatever task they find first.

The linear scan for unclaimed tasks looks expensive, but in practice it completes in under 50 microseconds for queues up to 10,000 tasks. The overhead becomes measurable only when task execution drops below 100 microseconds, at which point the problem is better suited for GPU compute or SIMD anyway.

Production deployments add monitoring by reserving extra shared memory for counters: total tasks claimed, total completed, peak queue depth. Workers increment these atomically to expose real-time telemetry without serialization overhead.

Complex worker pipelines often need barrier synchronization—all workers must reach a checkpoint before any can proceed. The Atomics.wait/notify

primitives enable efficient barriers without polling. A coordinator worker counts arrivals with atomic increments and wakes the group when the count reaches the target.

class WorkerBarrier {
  private state: Int32Array;
  private workerCount: number;
  private INDEX_ARRIVED = 0;
  private INDEX_GENERATION = 1;

  constructor(sharedBuffer: SharedArrayBuffer, workerCount: number) {
    this.state = new Int32Array(sharedBuffer, 0, 2);
    this.workerCount = workerCount;

    Atomics.store(this.state, this.INDEX_ARRIVED, 0);
    Atomics.store(this.state, this.INDEX_GENERATION, 0);
  }

  async wait(): Promise<void> {
    const generation = Atomics.load(this.state, this.INDEX_GENERATION);
    const arrived = Atomics.add(this.state, this.INDEX_ARRIVED, 1) + 1;

    if (arrived === this.workerCount) {
      // Last worker to arrive: reset and wake others
      Atomics.store(this.state, this.INDEX_ARRIVED, 0);
      Atomics.add(this.state, this.INDEX_GENERATION, 1);
      Atomics.notify(this.state, this.INDEX_GENERATION, this.workerCount - 1);
    } else {
      // Wait for generation to increment
      while (Atomics.load(this.state, this.INDEX_GENERATION) === generation) {
        const result = Atomics.wait(this.state, this.INDEX_GENERATION, generation, 1000);

        if (result === 'timed-out') {
          console.warn('Barrier timeout: not all workers arrived');
          break;
        }
      }
    }
  }
}

// Multi-phase computation with barriers
async function multiPhaseWorker(
  workerId: number,
  barrier: WorkerBarrier,
  sharedData: Float32Array
) {
  // Phase 1: Compute local results
  const localResult = computePhase1(workerId, sharedData);
  Atomics.store(sharedData, workerId, localResult);

  // Wait for all workers to finish phase 1
  await barrier.wait();

  // Phase 2: Aggregate results from all workers
  const aggregate = Array.from({ length: sharedData.length }, (_, i) =>
    Atomics.load(sharedData, i)
  ).reduce((sum, val) => sum + val, 0);

  // Use aggregate for phase 2 computation
  const phase2Result = computePhase2(workerId, aggregate);
  Atomics.store(sharedData, workerId, phase2Result);

  // Wait for all workers to finish phase 2
  await barrier.wait();

  // Phase 3: Final local processing
  const finalResult = computePhase3(workerId, sharedData);
  return finalResult;
}

The generation counter prevents ABA issues where a slow worker from the previous barrier round wakes up late and confuses itself with the current round. Each barrier cycle increments the generation, so workers check the generation value before waiting and only proceed when it changes.

This matters for pipelines like distributed ray tracing where each frame requires multiple synchronized passes. Workers trace rays in parallel, accumulate samples to shared buffers, and wait at a barrier before the next pass. Without barriers, workers would read partially-updated data from other workers and produce incorrect results.

The timeout mechanism is essential for production resilience. If one worker crashes mid-computation, the timeout ensures other workers don't deadlock waiting forever. Production systems detect timeouts and abort the entire computation rather than proceeding with incomplete data.

Message passing through postMessage

imposes serialization cost proportional to payload size. Structured cloning copies every field recursively, which takes 50-100 nanoseconds per field. A 1MB typed array with 262,144 float values takes 15-20ms to clone. Transferables eliminate the copy by moving ownership, but the sender loses access permanently.

SharedArrayBuffer eliminates both problems by keeping data in place and granting concurrent access. The write operation is a direct memory store—no serialization, no ownership transfer. For payloads above 100KB, shared memory outperforms message passing by 10-100x depending on payload structure.

The implication here is that small payloads (flags, counters, small strings) still favor postMessage

for simplicity. The overhead is negligible—under 1ms—and the ergonomics are better. Shared memory shines when payloads grow large or when latency budgets are tight.

Transferables occupy a middle ground. They match shared memory's speed for one-shot transfers but break down when data needs to ping-pong between workers. A video encoder that transfers frames to a worker, gets them back after encoding, and transfers again pays the setup cost three times. Shared memory pays it once.

Benchmark data from a 2026 production video pipeline:

Operation Payload Size postMessage Transferable SharedArrayBuffer
Send frame 8.3MB (4K) 22ms 1.2ms 0.008ms
Round-trip 8.3MB 44ms 2.4ms 0.016ms
60fps budget ❌ Misses ✓ Meets ✓ Meets

The failure mode here is choosing the wrong primitive for the workload. A chat application sending 500-byte messages every second wastes effort on shared memory. A DAW processing 96kHz audio streams fails with message passing.

The decision tree starts with latency requirements. If the system can tolerate 10ms+ delays, message passing is simpler and sufficient. Real-time systems with sub-5ms budgets demand shared memory. This distinction is critical because the complexity cost of shared memory—synchronization bugs, race conditions, memory layout decisions—only pays off when message passing fundamentally cannot meet the performance target.

Data ownership patterns matter. If workers operate on disjoint data sets (embarrassingly parallel problems), transferables win on simplicity. If multiple workers read and write the same data concurrently, shared memory is the only option. The implication here is that systems combining both patterns—large immutable payloads transferred, small mutable state shared—get the best of both worlds.

Security requirements impose hard constraints. Cross-origin isolation blocks third-party widgets, embedded iframes, and certain analytics scripts. Teams must audit dependencies and ensure all resources either live on the same origin or opt in with CORP headers. For applications heavily reliant on third-party integrations, this requirement might block SharedArrayBuffer

adoption entirely.

Debugging shared memory systems demands new tooling. Chrome DevTools shows shared buffer contents, but race conditions remain invisible until they cause corruption. Production teams instrument critical sections with atomic counters that track entry/exit, exposing concurrency bugs through anomalies in the telemetry. A counter that decrements more than it increments signals a missed store or torn read.

The maintenance burden grows with complexity. A ring buffer requires 200 lines of careful atomic coordination. A message-passing equivalent is 20 lines of straightforward postMessage

calls. The 10x complexity multiplier only makes sense when the performance delta is comparably large—which it is for real-time audio, video encoding, high-frequency trading, and scientific simulation, but not for most CRUD applications.

When to choose shared memory:

When to stick with message passing:

postMessage

Yes, but Atomics.wait

cannot block the main thread because that would freeze the UI. The main thread can create shared buffers, perform atomic operations like store

and compareExchange

, and call Atomics.notify

to wake workers. Only workers can call Atomics.wait

to suspend execution.

The browser disables SharedArrayBuffer

entirely, throwing an error if code tries to construct one. The application must fall back to message passing or transferables. As of 2026, all major browsers enforce this requirement consistently with no opt-out.

Use atomic operations for all concurrent access to shared state. Never mix atomic and non-atomic access to the same memory location. Structure data so each worker owns distinct regions, and use atomic indices or flags to coordinate ownership transfers. Thorough testing under high concurrency reveals most race conditions.

Yes, both browsers shipped stable support for SharedArrayBuffer

with cross-origin isolation in 2021-2022. As of 2026, the API surface is consistent across Chrome, Firefox, Safari, and Edge. Older browsers from before 2020 lack support entirely.

The specification allows up to 2^53 bytes (8 petabytes), but practical limits depend on available RAM and browser implementation. Most browsers cap individual buffers at 2GB to prevent abuse. Systems needing larger data sets should split across multiple buffers or use file-backed storage with incremental .

That covers the essential patterns for cross-worker shared state. Apply these in production and the difference will be immediate—audio pipelines that stuttered under message-passing latency stabilize, video encoders that dropped frames hit their deadlines, and data processing pipelines that serialized multi-megabyte structures every tick eliminate the bottleneck entirely. The complexity cost of SharedArrayBuffer

and atomics is real, but for workloads with tight latency budgets or large payloads, the performance payoff justifies the investment. The web platform now ships this capability reliably across all major browsers. Teams that master it unlock performance headroom that message passing fundamentally cannot match.

For more JavaScript performance patterns, see 11 JavaScript Examples to Source Code That Reveal Design Patterns in Use, 10 JavaScript Practices You Should Know Before Tomorrow, and 10 JavaScript and Node.js Tips That Knock Away Multiple Concepts.

── more in #developer-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/javascript-atomics-a…] indexed:0 read:18min 2026-07-13 ·