Skip to content

Feature Request: Add hibernation support for outgoing WebSocket connections in Durable Objects #4864

Description

@schickling

Summary

Add hibernation support for outgoing WebSocket connections created by Durable Objects using new WebSocket(). Currently, hibernation only works for incoming WebSocket connections accepted via ctx.acceptWebSocket(), but not for outgoing connections that a Durable Object initiates to external services.

Current State

  • ✅ Durable Objects support hibernation for incoming WebSocket connections via ctx.acceptWebSocket(socket)
  • ✅ This works when a DO accepts a WebSocket connection from an HTTP upgrade request
  • No hibernation support for outgoing WebSocket connections created via new WebSocket('some-url')

Problem

Durable Objects that maintain persistent connections to external WebSocket services cannot hibernate and will remain pinned in memory indefinitely. This prevents cost optimization during idle periods and limits the scalability benefits of hibernation.

Use Cases

Many real-world applications need Durable Objects to maintain persistent connections to external services while benefiting from hibernation:

  • Database connections: Real-time database subscriptions (Supabase, Firebase, etc.)
  • Message brokers: Persistent connections to Redis Streams, Apache Kafka, etc.
  • Real-time APIs: WebSocket connections to external real-time services
  • Inter-service communication: WebSocket connections between different parts of a distributed system

Proposed Solution

Extend the existing ctx.acceptWebSocket() method to work with outgoing WebSocket instances, maintaining API consistency with the current hibernation infrastructure:

const ws = new WebSocket('wss://external-service.com/ws');
ctx.acceptWebSocket(ws); // Should work for outgoing connections too

This approach leverages the existing hibernation handler methods (webSocketMessage(), webSocketClose(), webSocketError()) for both incoming and outgoing connections.

Expected Behavior

When a Durable Object hibernates with outgoing WebSocket connections:

  1. Connection persistence: WebSocket connections remain alive during hibernation
  2. Wake on message: When messages arrive on these connections, the DO wakes up automatically
  3. Handler invocation: The appropriate webSocketMessage(), webSocketClose(), or webSocketError() handlers are called after wake-up
  4. State restoration: The DO can access and interact with WebSocket instances after waking up
  5. Transparent operation: From the application's perspective, hibernation should be transparent

Implementation Example

export default {
  async fetch(request, env, ctx) {
    const id = env.MY_DURABLE_OBJECT.idFromName('example');
    const obj = env.MY_DURABLE_OBJECT.get(id);
    return await obj.fetch(request);
  }
}

export class MyDurableObject {
  constructor(ctx, env) {
    this.ctx = ctx;
    this.env = env;
  }

  async fetch(request) {
    // Create outgoing WebSocket connection
    if (\!this.externalWs) {
      this.externalWs = new WebSocket('wss://external-api.com/stream');
      
      // Enable hibernation for outgoing WebSocket (proposed feature)
      this.ctx.acceptWebSocket(this.externalWs);
      
      // Tag the WebSocket to distinguish from incoming connections
      this.externalWs.serializeAttachment({ 
        type: 'outgoing', 
        source: 'external-api' 
      });
    }
    
    return new Response('Outgoing WebSocket connection established');
  }

  // Same handler methods used for BOTH incoming AND outgoing WebSockets
  async webSocketMessage(ws, message) {
    // Handler distinguishes between different WebSocket sources
    const attachment = ws.deserializeAttachment();
    
    if (attachment?.type === 'outgoing') {
      console.log('Received from external service:', message);
      // Process external data, potentially send to connected clients
      this.broadcastToClients(message);
    } else {
      console.log('Received from client:', message);
      // Process client data, potentially forward to external service
      if (this.externalWs?.readyState === WebSocket.OPEN) {
        this.externalWs.send(message);
      }
    }
  }

  async webSocketClose(ws, code, reason, wasClean) {
    const attachment = ws.deserializeAttachment();
    
    if (attachment?.type === 'outgoing') {
      console.log('External WebSocket closed:', code, reason);
      // Handle external connection loss, maybe attempt reconnection
      this.handleExternalDisconnection();
    } else {
      console.log('Client WebSocket closed:', code, reason);
      // Handle client disconnection
    }
  }

  async webSocketError(ws, error) {
    const attachment = ws.deserializeAttachment();
    console.log('WebSocket error:', attachment?.type || 'unknown', error);
  }

  broadcastToClients(message) {
    // Send data from external service to all connected clients
    this.ctx.getWebSockets().forEach(ws => {
      const attachment = ws.deserializeAttachment();
      if (attachment?.type \!== 'outgoing') {
        ws.send(message);
      }
    });
  }

  handleExternalDisconnection() {
    // Implement reconnection logic or cleanup
    this.externalWs = null;
  }
}

Key Benefits of This Approach

  1. API Consistency: Uses the same hibernation infrastructure and handler methods for both incoming and outgoing WebSockets
  2. Hibernation Support: When the DO hibernates and wakes up on messages from outgoing WebSockets, it calls the familiar webSocketMessage() handler
  3. Connection Management: ctx.getWebSockets() would return both incoming and outgoing hibernating WebSockets
  4. State Preservation: serializeAttachment()/deserializeAttachment() allows distinguishing between connection types
  5. Cost Optimization: DOs can hibernate during idle periods even with persistent external connections
  6. Better Resource Utilization: Reduced memory usage across the Cloudflare network
  7. Improved Scalability: Enable more complex real-time applications that need external integrations

Technical Considerations

  • Connection Identification: WebSocket instances need to be tagged (via serializeAttachment()) so handlers can distinguish between incoming vs outgoing connections
  • Handler Unification: This removes the need for standard WebSocket event callbacks (onmessage, onclose, etc.) which don't work with hibernation
  • Backward Compatibility: The existing ctx.acceptWebSocket() method would be extended to work with outgoing WebSocket instances without breaking changes
  • Memory Management: All hibernation benefits (memory efficiency, cost optimization) apply to outgoing connections

Alternative Workarounds

Currently, developers must choose between suboptimal solutions:

  1. No hibernation: Keep DOs active permanently (expensive, poor resource utilization)
  2. Connection recreation: Constantly reconnect to external services (unreliable, connection overhead)
  3. Hybrid architecture: Use separate services for external connections (increased complexity)

None of these workarounds provide the optimal developer experience or cost efficiency that hibernation with outgoing WebSocket support would enable.

Related Documentation

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions