iToverDose/Software· 25 MAY 2026 · 04:00

XState actor persistence solved: A five-year-old challenge gets a self-hosted fix

Developers have struggled for years with XState’s lack of built-in persistence for state machines. A new open-source tool now offers a self-hosted, language-agnostic way to keep actors alive through restarts and migrations without reinventing the wheel.

DEV Community4 min read0 Comments

In 2019, a developer opened a GitHub issue in the XState repository asking how to persist state machines between server restarts. The question quickly became one of the most upvoted unresolved issues in the project. The core team’s response was candid: XState doesn’t handle persistence—you must serialize the state object and store it yourself. Five years later, that guidance hasn’t changed, and countless developers still find themselves rebuilding the same infrastructure over and over.

Some teams turn to third-party services like Stately Cloud, which offers a hosted solution for persisting XState actors. While functional for smaller projects, Stately Cloud introduces limitations that make it impractical for many enterprise use cases. Data residency, language lock-in, and the absence of robust migration tools create friction for teams in regulated industries or polyglot environments. These gaps aren’t flaws in XState—they’re symptoms of a missing layer between the library and the application database.

The hidden cost of rolling your own persistence

At first glance, persisting XState state seems simple: call actor.getSnapshot(), serialize it, store it in Redis or PostgreSQL, and rehydrate on startup. The approach works—until it doesn’t. XState v5 introduced a breaking change to the snapshot format in 2023, leaving teams scrambling to migrate stored states that suddenly failed to deserialize or corrupted context silently. The snapshot format was never intended as a public API, which means library maintainers aren’t obligated to support backward compatibility.

Even before version changes, the migration problem looms large. Consider an order management workflow with 40,000 active orders. A product team decides to add a quality review step to the approval process. A migration script is written, tested in staging, and deployed at midnight. Only after the fact do developers discover that 847 orders were stuck in an unanticipated edge state, requiring hours of manual fixes. This isn’t a failure of XState or developer skill—it’s a gap in infrastructure. There’s no standard tooling to bridge the library and your application database while preserving actor lifecycles through code changes.

Why Stately Cloud isn’t the answer for everyone

Stately Cloud addresses some persistence needs by offering a hosted service that keeps XState actors alive between restarts and provides an API for sending events and reading state. It’s a practical solution for side projects or JavaScript-only teams comfortable with third-party data handling. However, three critical limitations make it unsuitable for many organizations:

  • Data residency restrictions: For fintech, healthcare, insurance, or enterprise SaaS teams with strict compliance requirements, sending customer data to a third-party hosted service is often non-negotiable. Stately Cloud lacks a self-hosted deployment option.
  • Language exclusivity: The service requires actual XState TypeScript implementations. Teams running Python, Go, Java, or other backend languages are locked out entirely.
  • No support for path-based migrations: When updating a workflow definition, Stately Cloud doesn’t help determine which in-flight actors should migrate to the new version. You’re on your own to write that logic, which can introduce errors at scale.

The migration challenge: Why most tools fail

Migration sounds straightforward but unravels under real-world complexity. Imagine a loan application workflow with 50,000 active applications. A new compliance check step must be added—but only for applicants who paid a verification fee, as regulatory requirements demand. Both paid and waived applicants currently sit in the awaiting_documents state, making them indistinguishable to any system querying current state. A migration tool that routes actors based solely on their current state would treat them identically, which is incorrect.

The correct approach requires inspecting each actor’s history. An applicant who processed the PAY_FEE event belongs in the new compliance check flow, while one who processed WAIVE_FEE does not. The only way to determine which group an actor belongs to is by examining the sequence of events it has already processed. Most migration tools ignore this nuance, leaving teams to build fragile workarounds.

How StateKeep solves the persistence and migration puzzle

StateKeep is a self-hosted platform designed to eliminate these pain points. It lets you deploy XState-compatible JSON definitions via HTTP, spawn actors, and send events from any backend language with an HTTP client. When deploying a new workflow version, you declare a historyPath that specifies which event sequences trigger migration:

{
  "id": "loan-v2",
  "parentId": "loan-v1",
  "historyPath": ["SUBMIT_INFO", "PAY_FEE"],
  "definition": { ... }
}

StateKeep evaluates every actor using a rolling FNV-1a hash of its event history. Actors whose history matches the declared path migrate to the new version, while others continue running the current definition. This ensures Alice, who paid the fee, moves to loan-v2 for compliance checks, while Bob, who waived the fee, stays on loan-v1 without interruption.

The system guarantees correctness through a formal mathematical proof ensuring no actor is evaluated twice and every actor lands on the correct version, regardless of timing or evaluation order. The migration algorithm runs in native C, averaging 1.26 microseconds per actor—for 50,000 actors, that’s under a second on modest hardware.

Putting StateKeep into practice

Integrating StateKeep into your workflow is straightforward. The client SDK handles deployment, event sending, and state retrieval:

import { createClient } from '@statekeep/sdk';

const sk = createClient({
  baseUrl: '
  apiKey: 'sk_...'
});

// Deploy a machine definition
await sk.deploy('loan-v1', {
  id: 'loan',
  initial: 'submitted',
  states: {
    submitted: {
      on: { SUBMIT_INFO: 'under_review' }
    },
    under_review: {
      on: { PAY_FEE: 'awaiting_income' }
    }
  }
});

// Send events to actors
await sk.send('loan-12345', 'PAY_FEE');

// Query state
const state = await sk.state('loan-12345');

By addressing the persistence and migration gaps that have plagued XState users for years, StateKeep offers a self-hosted, language-agnostic alternative that scales with your infrastructure needs. For teams tired of reinventing the same infrastructure, it’s a step toward building resilient state machines without sacrificing control or compliance.

AI summary

XState kalıcılık problemi artık StateKeep ile çözülüyor. StateKeep, XState aktörlerinizi kalıcı hale getiren ve canlı tutan self-hosted bir platformdur.

Comments

00
LEAVE A COMMENT
ID #K8QPJ1

0 / 1200 CHARACTERS

Human check

8 + 9 = ?

Will appear after editor review

Moderation · Spam protection active

No approved comments yet. Be first.