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:
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
};
}
);
Handler invoked
Resource handler called with phase “create”
Infrastructure provisioned
API calls create the actual infrastructure
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...
}
);
Props comparison
Alchemy detects props have changed from state
Handler invoked
Resource handler called with phase “update”
Infrastructure updated
API calls update the existing infrastructure
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...
}
);
Orphan detection OR explicit destroy
Resource exists in state but not in code, or app in destroy phase
Handler invoked
Resource handler called with phase “delete”
Infrastructure deleted
API calls remove the infrastructure
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:
Detect immutable change
this.replace() is called during update phase
Mark for replacement
Old resource is added to pending deletions
Create new resource
Handler is called again in “create” phase with isReplacement: true
Delete old resource
After new resource succeeds, old resource is destroyed
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:
Execute deferred operations
Operations registered with scope.defer() run
Finalize child scopes
Recursively finalize nested scopes
Detect orphaned resources
Compare state with resources created in code
Destroy pending deletions
Delete resources marked for replacement
Destroy orphans
Delete resources no longer in code
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
Handle all phases
Implement create, update, and delete logic completely
Validate immutability
Call this.replace() when immutable properties change
Graceful deletion
Handle 404 errors during deletion (resource already gone)
Always finalize
Call await app.finalize() to clean up orphans
Use deferred operations
Defer post-deployment tasks until all resources exist
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