Skip to main content

Lifecycle

Alchemy manages infrastructure through a well-defined lifecycle that handles creation, updates, and deletion of resources. Understanding this lifecycle is essential for building reliable infrastructure.

Deployment Phases

Alchemy applications operate in one of three phases:

Up Phase (Default)

The up phase creates and updates resources:
bun ./alchemy.run.ts
const app = await alchemy("my-app", {
  phase: "up"  // Default, usually omitted
});

const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// Resource is created or updated

await app.finalize();
The up phase is the default. You typically don’t need to specify it explicitly.

Destroy Phase

The destroy phase deletes all resources:
bun ./alchemy.run.ts --destroy
const app = await alchemy("my-app", {
  phase: "destroy"
});

const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// Resource is deleted

await app.finalize();
Destroy phase permanently deletes infrastructure. Use with caution, especially in production.

Read Phase

The read phase retrieves resource state without making changes:
bun ./alchemy.run.ts --read
const app = await alchemy("my-app", {
  phase: "read"
});

const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// Returns existing resource from state
console.log(worker.url);
// No changes are made

await app.finalize();
Read phase is useful for inspecting deployed infrastructure or in multi-app deployments where one app depends on another.

Resource Lifecycle Phases

Individual resources go through their own lifecycle phases:

Create Phase

When a resource doesn’t exist in state:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    console.log("Phase:", this.phase); // "create"
    console.log("Output:", this.output); // undefined
    console.log("Props:", this.props); // undefined
    
    // Provision new infrastructure
    const result = await api.createResource({
      name: props.name,
      config: props.config
    });
    
    return {
      id,
      name: result.name,
      resourceId: result.id
    };
  }
);
1

Handler invoked

Resource handler called with phase “create”
2

Infrastructure provisioned

API calls create the actual infrastructure
3

State saved

Return value is saved to state with status “created”

Update Phase

When resource exists but props have changed:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    if (this.phase === "update") {
      console.log("Previous output:", this.output);
      console.log("Previous props:", this.props);
      
      // Check for immutable property changes
      if (this.output.name !== props.name) {
        // Name is immutable - trigger replacement
        return this.replace();
      }
      
      // Update mutable properties
      const result = await api.updateResource(
        this.output.resourceId,
        { config: props.config }
      );
      
      return {
        id,
        name: this.output.name,
        resourceId: this.output.resourceId,
        config: result.config
      };
    }
    
    // Create logic...
  }
);
1

Props comparison

Alchemy detects props have changed from state
2

Handler invoked

Resource handler called with phase “update”
3

Infrastructure updated

API calls update the existing infrastructure
4

State updated

New state saved with old props preserved in oldProps

Delete Phase

When resource is removed from code or explicitly destroyed:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    if (this.phase === "delete") {
      console.log("Deleting:", this.output);
      
      // Clean up infrastructure
      try {
        await api.deleteResource(this.output.resourceId);
      } catch (error) {
        if (error.status !== 404) {
          throw error; // Re-throw unless already deleted
        }
      }
      
      // Signal deletion complete
      return this.destroy();
    }
    
    // Create/update logic...
  }
);
1

Orphan detection OR explicit destroy

Resource exists in state but not in code, or app in destroy phase
2

Handler invoked

Resource handler called with phase “delete”
3

Infrastructure deleted

API calls remove the infrastructure
4

State removed

this.destroy() deletes the state file
Always call this.destroy() in the delete phase. Without it, Alchemy won’t remove the state.

Lifecycle Transitions

First Deployment

const app = await alchemy("my-app");

const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// Phase: create
// Creates worker and saves state

await app.finalize();
State file created:
{
  "kind": "cloudflare::Worker",
  "id": "api",
  "status": "created",
  "props": { "entrypoint": "./src/api.ts" },
  "output": { "url": "https://api.worker.dev" }
}

Second Deployment (No Changes)

const app = await alchemy("my-app");

const worker = await Worker("api", {
  entrypoint: "./src/api.ts"
});
// Props unchanged - no update needed
// Returns existing state

await app.finalize();
If props haven’t changed, Alchemy skips the update unless --force is used.

Third Deployment (Props Changed)

const app = await alchemy("my-app");

const worker = await Worker("api", {
  entrypoint: "./src/api.ts",
  compatibilityFlags: ["nodejs_compat"]  // New prop
});
// Phase: update
// Updates worker with new config

await app.finalize();
State updated:
{
  "kind": "cloudflare::Worker",
  "id": "api",
  "status": "updated",
  "props": { 
    "entrypoint": "./src/api.ts",
    "compatibilityFlags": ["nodejs_compat"]
  },
  "oldProps": { "entrypoint": "./src/api.ts" },
  "output": { "url": "https://api.worker.dev" }
}

Fourth Deployment (Resource Removed)

const app = await alchemy("my-app");

// Worker not created anymore

await app.finalize();
// Orphan detected: "api" exists in state but not in code
// Phase: delete
// Worker destroyed and state removed

Resource Replacement

Some changes require replacing a resource entirely:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    // Check for immutable property changes
    if (this.phase === "update" && this.output.region !== props.region) {
      // Region can't be changed - must recreate
      return this.replace();
    }
    
    // Normal create/update logic
  }
);
Replacement workflow:
1

Detect immutable change

this.replace() is called during update phase
2

Mark for replacement

Old resource is added to pending deletions
3

Create new resource

Handler is called again in “create” phase with isReplacement: true
4

Delete old resource

After new resource succeeds, old resource is destroyed
5

Update state

State points to new resource
Replacement causes downtime as the old resource is deleted before the new one is fully ready.

Finalization

The finalization process runs after all resources are created:
const app = await alchemy("my-app");

// Create resources
const worker = await Worker("api", { ... });
const bucket = await R2Bucket("storage", { ... });

// Finalize - critical step!
await app.finalize();
Finalization steps:
1

Execute deferred operations

Operations registered with scope.defer() run
2

Finalize child scopes

Recursively finalize nested scopes
3

Detect orphaned resources

Compare state with resources created in code
4

Destroy pending deletions

Delete resources marked for replacement
5

Destroy orphans

Delete resources no longer in code
6

Run cleanup handlers

Execute functions registered with scope.onCleanup()
Forgetting await app.finalize() means orphaned resources won’t be cleaned up!

Deferred Operations

Defer operations until finalization:
const app = await alchemy("my-app");

const worker = await Worker("api", { ... });

// Defer operation until finalization
const result = app.defer(async () => {
  // This runs during app.finalize()
  const response = await fetch(worker.url);
  return response.text();
});

await app.finalize();

// Now the deferred operation has completed
const text = await result;
console.log(text);
Deferred operations are useful for post-deployment validations or setup that requires all resources to exist.

Cleanup Handlers

Register cleanup for process exit:
const app = await alchemy("my-app");

await alchemy.run("dev-server", async (scope) => {
  const server = await startServer();
  
  // Register cleanup
  scope.onCleanup(async () => {
    console.log("Shutting down server...");
    await server.close();
  });
});

await app.finalize();

// When process exits (Ctrl+C, etc.):
// - Cleanup handlers run
// - Server is gracefully shut down

Destroy Strategies

Control how resources are destroyed:

Sequential (Default)

Destroy resources one at a time:
const app = await alchemy("my-app", {
  destroyStrategy: "sequential"
});

Parallel

Destroy all resources concurrently:
const app = await alchemy("my-app", {
  destroyStrategy: "parallel"
});

Per-Resource Strategy

Override strategy for specific resources:
export const MyResource = Resource(
  "provider::MyResource",
  { destroyStrategy: "parallel" },
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    // Implementation
  }
);
Sequential is safer but slower. Parallel is faster but may cause issues if resources have dependencies.

Force Mode

Force updates even when props haven’t changed:
bun ./alchemy.run.ts --force
const app = await alchemy("my-app", {
  force: true
});
Use cases:
  • Recover from state corruption
  • Apply provider-side changes
  • Debug update logic

Adoption

Adopt existing resources:
bun ./alchemy.run.ts --adopt
const app = await alchemy("my-app", {
  adopt: true
});

const worker = await Worker("existing-worker", {
  name: "my-existing-worker"
});
// If worker exists: adopts it
// If worker doesn't exist: creates it
Adoption is useful for migrating existing infrastructure to Alchemy management.

Error Handling

Handling errors during lifecycle:
export const MyResource = Resource(
  "provider::MyResource",
  async function (this: Context<MyResource>, id: string, props: MyResourceProps) {
    if (this.phase === "delete") {
      try {
        await api.deleteResource(this.output.resourceId);
      } catch (error) {
        // Handle deletion errors gracefully
        if (error.status === 404) {
          // Already deleted - OK
        } else if (error.message.includes("in use")) {
          // Resource is in use - warn but continue
          console.warn(`Resource ${id} still in use, skipping deletion`);
        } else {
          // Unexpected error - fail
          throw error;
        }
      }
      
      return this.destroy();
    }
    
    // Create/update logic
  }
);

Best Practices

1

Handle all phases

Implement create, update, and delete logic completely
2

Validate immutability

Call this.replace() when immutable properties change
3

Graceful deletion

Handle 404 errors during deletion (resource already gone)
4

Always finalize

Call await app.finalize() to clean up orphans
5

Use deferred operations

Defer post-deployment tasks until all resources exist
6

Register cleanups

Use scope.onCleanup() for process exit handlers

Next Steps

Resources

Implement resources with proper lifecycle handling

State Management

Understand how state drives the lifecycle

Scopes

Manage scope-level lifecycle and finalization

Deployment

Deploy infrastructure using lifecycle phases

Build docs developers (and LLMs) love