Skip to main content
The I/O subsystem manages worker lifecycle, request handling, actor storage, and concurrency control. It bridges the JavaScript runtime with the underlying system I/O.

Overview

The I/O subsystem is responsible for:
  • Managing worker isolates and scripts
  • Tracking per-request context and resources
  • Implementing Durable Object storage and caching
  • Enforcing consistency guarantees with gates
  • Bridging KJ promises with JavaScript promises

Key components

IoContext

The per-request “god object” that tracks all state for a single request:
class IoContext {
  // Get the current request's IoContext (thread-local)
  static IoContext& current();

  // Bridge KJ promise to JS promise
  template <typename T>
  jsg::Promise<T> awaitIo(
      kj::Promise<T> promise,
      jsg::Function<T(jsg::Lock&, T)> callback);

  // Bridge JS promise to KJ promise
  template <typename T>
  kj::Promise<T> awaitJs(jsg::Promise<T> promise);

  // Safely store I/O objects accessible from JS
  template <typename T>
  IoOwn<T> addObject(kj::Own<T> obj);
};
Access via thread-local:
void myFunction() {
  auto& context = IoContext::current();
  // Use context
}
IoContext::current() is only valid during request handling. Attempting to access it outside of a request throws an exception.

Worker hierarchy

The worker system has three levels:
// Isolate: V8 isolate wrapper, shared across workers
class Worker::Isolate {
  // Create a new isolate
  static kj::Ref<Isolate> create(/* ... */);

  // Compile a script in this isolate
  kj::Ref<Script> newScript(/* ... */);
};

// Script: Compiled code bound to an isolate
class Worker::Script {
  // Create a worker instance from this script
  kj::Ref<Worker> createWorker(/* ... */);
};

// Worker: Ref-counted worker instance
class Worker {
  // Handle a request
  kj::Promise<void> handleRequest(/* ... */);

  // Get an actor instance
  kj::Ref<Actor> getActor(kj::StringPtr id);
};
Relationships:
  • Multiple Scripts can share one Isolate (same compatibility settings)
  • Multiple Workers can share one Script (same code)
  • Each Worker has its own global scope

Worker locks

// Synchronous V8 isolate lock
class Worker::Lock {
  // Must hold to touch JS heap
  Lock(Worker& worker, LockType type);

  jsg::Lock& getJsgLock();
};

// Fair async queue for locks
class Worker::AsyncLock {
  // Wait for exclusive access
  kj::Promise<Worker::Lock> takeAsyncLock();

  // Wait with request tracking
  kj::Promise<Worker::Lock> takeAsyncLockWithoutRequest(/* ... */);
};
Usage pattern:
auto asyncLock = worker.getAsyncLock();
co_await asyncLock.takeAsyncLock().then(
    [](Worker::Lock lock) {
  // Now safe to access JS heap
  auto& js = lock.getJsgLock();
  // Do work
});

Actor storage

ActorCache

LRU write-back cache over RPC storage:
class ActorCache {
  // Get a value
  kj::OneOf<kj::Maybe<Value>, kj::Promise<kj::Maybe<Value>>>
  get(Key key);

  // Put a value
  kj::OneOf<void, kj::Promise<void>>
  put(Key key, Value value);

  // List keys
  kj::OneOf<ResultList, kj::Promise<ResultList>>
  list(ListOptions options);

  // Delete a key
  kj::OneOf<bool, kj::Promise<bool>>
  delete_(Key key);
};
Key features:
  • Returns kj::OneOf<Result, kj::Promise<Result>> - synchronous when cached, async otherwise
  • Write-back caching - writes are batched and flushed periodically
  • LRU eviction - least recently used entries are evicted under memory pressure
  • Transactional semantics - operations within an output gate are atomic

ActorSqlite

SQLite-backed storage implementation:
class ActorSqlite: public ActorCacheOps {
  // Implements ActorCacheOps interface
  kj::OneOf<Value, kj::Promise<Value>> get(Key key) override;
  kj::OneOf<void, kj::Promise<void>> put(Key key, Value value) override;
  // ...
};
Features:
  • Persistent storage backed by SQLite
  • No nested transactions (throws SQLITE_MISUSE)
  • Optimized for Durable Object workloads

Consistency gates

Gates enforce ordering and consistency guarantees for Durable Objects.

InputGate

Controls when requests can start processing:
class InputGate {
  // Wait until gate is open
  kj::Promise<void> wait();

  // Critical section that must succeed or permanently break gate
  kj::Promise<void> onBroken();
};
Sematics:
  • Requests wait at the input gate before processing
  • If a critical section fails, the gate breaks permanently
  • Used to ensure requests see consistent state

OutputGate

Controls when responses can be sent:
class OutputGate {
  // Hold response until promise resolves
  template <typename T>
  kj::Promise<T> lockWhile(kj::Promise<T> promise);
};
Semantics:
  • Responses are held until the output gate releases them
  • Ensures side effects complete before responding
  • Used for waitUntil() and background tasks
Example:
auto work = doBackgroundWork();
outputGate.lockWhile(kj::mv(work));
// Response will not be sent until work completes

Cross-heap safety

IoOwn and IoPtr

Prevent dangling references between KJ I/O objects and JS heap:
// Owned pointer checked against request lifetime
template <typename T>
class IoOwn {
  T& operator*();
  T* operator->();
};

// Borrowed pointer checked against request lifetime
template <typename T>
class IoPtr {
  T& operator*();
  T* operator->();
};

// Reverse ownership: JS owns KJ object
template <typename T>
class ReverseIoOwn {
  // ...
};
Usage:
class MyResource: public jsg::Object {
  void storeClient(kj::Own<HttpClient> client) {
    // BAD: Direct storage allows cross-request access
    // this->client = kj::mv(client);

    // GOOD: IoOwn prevents cross-request access
    auto& context = IoContext::current();
    this->client = context.addObject(kj::mv(client));
  }

  void visitForGc(jsg::GcVisitor& visitor) {
    visitor.visit(client);
  }

private:
  IoOwn<HttpClient> client;
};
Never store raw KJ I/O objects (HttpClient, ActorCache, etc.) in objects reachable from the JS heap. Always use IoOwn<T>.

Promise bridging

Bridge between KJ promises (C++ I/O) and JavaScript promises.

KJ to JavaScript

jsg::Promise<Response> fetch(
    jsg::Lock& js,
    kj::String url) {
  // Start KJ I/O operation
  auto kjPromise = httpClient.request(kj::mv(url));

  // Bridge to JS with continuation
  return js.awaitIo(kj::mv(kjPromise),
      [](jsg::Lock& js, HttpResponse response) {
    // This callback runs under V8 lock
    return js.alloc<Response>(kj::mv(response));
  });
}

JavaScript to KJ

kj::Promise<kj::String> process(
    jsg::Lock& js,
    jsg::Promise<jsg::Ref<Response>> jsPromise) {
  // Bridge JS promise to KJ
  return js.awaitJs(kj::mv(jsPromise)).then(
      [](jsg::Lock& js, jsg::Ref<Response> response) {
    return response->getText();
  });
}
The callback passed to awaitIo() runs under the V8 lock and can safely access the JS heap.

Worker lifecycle

Request handling

  1. Request arrives at worker
  2. Acquire async lock
  3. Create IoContext for request
  4. Wait at input gate
  5. Execute JavaScript handler
  6. Wait for output gate
  7. Send response
  8. Clean up IoContext

Actor lifecycle

class Worker::Actor {
  // Get or create actor instance
  static kj::Ref<Actor> get(
      Worker& worker,
      kj::StringPtr id);

  // Make request to actor
  kj::Promise<Response> request(Request req);

  // Hibernate/resume for WebSockets
  void hibernate();
  void resume();
};
Actor features:
  • Single-threaded execution via input/output gates
  • Persistent storage via ActorCache/ActorSqlite
  • Hibernation for WebSocket connections
  • Automatic lifecycle management

Observers and hooks

Instrumentation points for metrics and tracing:
class RequestObserver {
  // Called when request starts
  virtual void onStart() {}

  // Called when request completes
  virtual void onComplete(Status status) {}
};

class IsolateObserver {
  // Called on dynamic eval
  virtual void onDynamicEval(/* ... */) {}

  // Called on module compilation
  virtual void onModuleCompile(/* ... */) {}
};

class ActorObserver {
  // Called on storage operation
  virtual void onStorageOp(/* ... */) {}

  // Called on alarm invocation
  virtual void onAlarm(/* ... */) {}
};
All observer methods are optional with no-op defaults.

Best practices

Use IoContext correctly

// GOOD: Get context when needed
void myFunction() {
  auto& context = IoContext::current();
  auto ioOwn = context.addObject(kj::mv(client));
}

// BAD: Store IoContext reference
class Bad {
  IoContext& context;  // Dangling after request ends
};

Promise bridging

// GOOD: Use awaitIo with continuation
return js.awaitIo(kjPromise, [](jsg::Lock& js, Result r) {
  return processResult(js, kj::mv(r));
});

// BAD: Use awaitIoLegacy (deprecated)
return js.awaitIoLegacy(kjPromise);  // Don't use in new code

Gate usage

// GOOD: Lock output gate for background work
void addWaitUntil(kj::Promise<void> promise) {
  outputGate.lockWhile(kj::mv(promise)).detach([](kj::Exception&& e) {
    // Log error
  });
}

// BAD: Forget to lock gate
void bad(kj::Promise<void> promise) {
  promise.detach([](kj::Exception&& e) {});
  // Response may be sent before promise completes
}

Next steps

API layer

Explore how runtime APIs are implemented

JSG architecture

Learn about JavaScript bindings

Build docs developers (and LLMs) love