Chapter 27: Migration Playbooks
How do I move existing workloads to Cloudflare, and when should I?
The previous chapter helped determine whether Cloudflare fits your workload. This chapter addresses the practical question: how do you get there?
Migration is not a goal; faster applications, lower costs, and simpler operations are goals. Migration is a means and an expensive one. Before planning how to migrate, establish why and whether expected benefits justify the certain costs. This chapter provides playbooks for common migration scenarios, assuming you've already decided migration is worthwhile.
The migration principles
Successful migrations share common principles regardless of source or target. These aren't best practices to consider. They're requirements that distinguish success from cautionary tales.
Coexistence before cutover
Run old and new systems in parallel, routing some traffic to the new system while the old remains operational. Validate behaviour, compare results, and build confidence. Only after the new system proves itself do you cut over completely.
New systems always misbehave in ways you didn't anticipate: edge cases in data, traffic patterns you didn't test, integrations that assumed behaviours your new system doesn't provide. Coexistence gives you time to discover these problems without user-facing outages.
The cost of running two systems is real but bounded; the cost of a failed atomic cutover is unbounded. Coexistence is insurance worth paying for.
Incremental over atomic
Migrate one service, one data store, one capability at a time; each increment is a chance to learn, adjust, and verify. Atomic migrations compound risks and create uncertainty about what failed if something goes wrong.
Incrementalism also manages organisational risk: a team migrating one service learns lessons they apply to the next, whereas a team migrating everything at once learns lessons they can only apply to the post-mortem.
The objection is usually "but the systems are interconnected, we can't migrate piece by piece," which is sometimes true but more often a failure of imagination. Most systems can be decomposed; the question is whether you've tried. Hybrid architectures are valid intermediate states, not failures to complete migration.
Reversibility as requirement
Define a recovery path before each migration step. Retaining S3 objects or an old Lambda deployment preserves useful assets, but a route back is safe only if the old system can read current state and account for writes accepted since cutover. Decide how those writes return, or how the new system will be repaired, before changing authority.
Prefer reversible experiments while assessing fit. Where an operation cannot be reversed, bound its scope and require stronger evidence before executing it. Chapter 23 explains why code rollback and application recovery are different procedures.
Maintaining old infrastructure is one cost of recovery; preserving compatible schemas, change history and usable credentials is another. Set a retirement window from the failure detection period and the work needed to reconcile state. An idle function alone is not rollback capability.
Observation before and during
Establish baseline metrics before migration (latency distributions, error rates, costs, user experience measures), then monitor the same metrics during and after. Without baselines, you can't know if migration improved anything; without continuous monitoring, you can't catch regressions until users report them.
The metrics that matter depend on why you're migrating. For latency, measure p50, p95, and p99 from representative geographic locations; for cost, track resource consumption at sufficient granularity to compare; for operational simplicity, measure time-to-deploy, incident frequency, and mean time to recovery.
"We migrated successfully" is not a useful claim without metrics. "We reduced p99 latency from 450ms to 120ms while reducing compute costs by 35%" is useful and requires observation; plan for it.
Zero-downtime migration architecture
Coexistence can preserve service while endpoints and data move. The design still needs a controlled transfer of writes and a recovery path for changes accepted during the transition. Use these mechanisms to test whether an uninterrupted cutover is achievable for the workload.
The core insight is that Cloudflare sits between your users and your infrastructure by design. Once traffic flows through Cloudflare's network, you control where it goes, how much goes where, and how quickly you can change your mind. This position makes Cloudflare uniquely suited to orchestrating its own adoption: the same network that will eventually run your application can manage the transition to get there.
The strangler fig pattern
The strangler fig is a tree that grows around its host, gradually replacing it while the host continues to function. The software equivalent, coined by Martin Fowler, describes incrementally replacing a legacy system by routing requests through a new layer that delegates to either new or old implementations.
Workers are natural strangler figs. Deploy a Worker in front of your existing infrastructure, initially proxying every request unchanged to your hyperscaler backend. This gives you a control point for the migration. Measure its added latency with representative requests, including the route from Cloudflare to the existing backend.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Migrated endpoints use new implementation
if (isMigrated(url.pathname)) {
return handleLocally(request, env);
}
// Everything else proxies to legacy infrastructure
return fetch(env.LEGACY_ORIGIN + url.pathname + url.search, {
method: request.method,
headers: request.headers,
body: request.body,
});
}
};
Use the proxy to move selected endpoints after their dependencies and state ownership are understood. A routing flag can live in KV when propagation delay is acceptable; an emergency writer fence needs an authoritative control. Test both request paths and the handoff, including authentication, streaming, retries and writes already in flight.
The pattern works because Cloudflare's network already terminates TLS and handles DNS for your domain. Adding a Worker to the request path is a configuration change, not an infrastructure migration. Your users' experience is unchanged; only the routing logic behind Cloudflare's edge has shifted.
Deploy a Worker that proxies everything to your existing backend before migrating anything. This validates the request path, establishes baseline metrics through Cloudflare's analytics, and gives you a control point for all future migration steps. Measure the added proxy path with representative requests before using it as the migration control point.
Gradual deployments and traffic splitting
Once your Worker handles some endpoints natively, you need confidence before routing all traffic through new code paths. Cloudflare's gradual deployments solve this precisely.
Gradual deployments split traffic between Worker versions by percentage. Deploy a new version handling /api/users natively, then route 0.5% of traffic to it while 99.5% continues using the proxy-to-legacy version. Monitor error rates, latency distributions, and response correctness at each stage. Stop promotion when the criteria fail. Route traffic back only while the previous version remains compatible with current state; otherwise isolate the affected path and follow its recovery procedure.
Choose rollout stages from exposure risk, traffic volume and the time needed to observe representative behaviour. A small percentage of a busy service may supply useful evidence quickly; the same percentage of a quiet one may miss an important workflow for days. Chapter 22 develops promotion and rollback criteria.
The critical advantage over DNS-based traffic splitting is speed. DNS changes propagate over minutes to hours depending on TTL settings and resolver caching behaviour. Gradual deployments take effect within seconds because the split happens at Cloudflare's edge, not in the DNS layer. Rolling back a DNS change means waiting for caches to expire; rolling back a gradual deployment means clicking a button.
Workers rollback can select from the last 100 published versions, subject to binding and migration restrictions. If version 47 corrupted data and the defect is discovered at version 52, returning to version 46 does not restore the data. Stop further damage, verify old-code compatibility and reconcile or restore affected state before reopening the path.
Connecting to existing infrastructure
Zero-downtime migration requires your new infrastructure to communicate with your old infrastructure throughout the transition. You cannot migrate everything simultaneously, so Workers need to reach backends still running on hyperscalers. Cloudflare provides two mechanisms for this, each suited to different scenarios.
Cloudflare Tunnel creates an outbound-only encrypted connection from your existing infrastructure to Cloudflare's edge. Install a lightweight connector (cloudflared) in your AWS VPC, Azure VNet, or GCP VPC, and it establishes a persistent tunnel without requiring any inbound firewall rules. Workers route requests through the tunnel to reach internal services, databases, or APIs that aren't publicly accessible.
The operational benefit during migration is substantial. You need not expose internal services to the public internet, reconfigure security groups, or punch holes in firewalls. The tunnel connector runs alongside your existing infrastructure and can be removed when migration completes. Multiple connectors provide high availability, and failover is automatic.
Workers VPC Services build on Tunnel to provide binding-level access to specific internal services. Rather than giving a Worker access to your entire private network (which creates the SSRF risks Chapter 1 discussed), VPC Services let you bind a Worker to a specific internal endpoint. The Worker accesses env.LEGACY_API.fetch() and the request routes through the tunnel to exactly that service, nothing else.
This distinction matters for security during migration. A Worker handling user requests shouldn't be able to reach your internal monitoring infrastructure or administrative APIs just because a tunnel exists. VPC Services enforce least-privilege access at the binding level, limiting each Worker to the specific backends it needs.
For database connectivity during migration, Hyperdrive provides the bridge. Configure Hyperdrive to connect to your existing PostgreSQL or MySQL database, whether it runs on RDS, Cloud SQL, Azure Database, or self-hosted infrastructure. Workers access the database through a Hyperdrive binding with connection pooling, prepared statement caching, and global connection reuse handled automatically. Your database stays where it is; your compute migrates around it.
Smart Placement during transition
When your Workers connect to backends still running on hyperscalers, latency depends on the distance between Worker execution and backend location. A Worker running in Sydney that queries an RDS instance in us-east-1 pays for a trans-Pacific round trip on every database call.
Smart Placement addresses this automatically. Enable it on Workers that make backend calls, and Cloudflare analyses traffic patterns to determine optimal execution location. Moving execution nearer the backend can reduce repeated database round trips while lengthening the user-to-compute path. Measure the complete request, including tail latency, before accepting the placement change.
For known, fixed backend locations, explicit placement hints provide immediate optimisation without waiting for Smart Placement to learn patterns. Specify "aws:us-east-1" in your placement configuration, and your Worker executes in the Cloudflare data centre closest to that AWS region from the first request.
Reassess placement as dependencies move. Native D1 and Durable Objects still have data locality, so moving every dependency to Cloudflare does not automatically make nearest-user execution optimal. Keep or disable placement based on measured end-to-end latency and the distribution of users and state.
The phased approach
These capabilities support coexistence while the application moves. Availability still depends on compatible interfaces, sufficient capacity and a proved handoff of authoritative writes. The phases overlap in practice, but each has its own completion evidence.
Phase one: establish the edge. Put Cloudflare in front of your existing infrastructure. This might mean proxying through Workers, or simply enabling Cloudflare as a DNS proxy for your domain. At this stage, nothing changes for your users. You gain Cloudflare's DDoS protection, TLS management, and analytics as immediate benefits while establishing the control point for subsequent phases. If your domain already uses Cloudflare for CDN or security, this phase is already complete.
Phase two: migrate storage incrementally. Configure Sippy on R2 with access to the S3 source. Reads for uncopied objects can fetch from that source and populate R2, provided the source remains available and accessible. Update the application’s endpoint and credentials, verify the S3 operations it uses, and establish which store accepts writes. Super Slurper can copy the rarely accessed remainder in bulk. The object-storage playbook below covers compatibility and cutover.
Phase three: migrate compute gradually. Deploy Workers handling migrated endpoints while proxying everything else to legacy infrastructure. Use gradual deployments to shift traffic incrementally: 0.5%, 3%, 10%, 25%, 50%, 100%. Enable Smart Placement so Workers execute near your legacy databases during the transition. Each endpoint migrates independently on its own timeline.
Phase four: migrate data selectively. With Workers using the existing database through Hyperdrive, assess whether a data migration earns its cost. If D1 fits, use the snapshot, durable change capture and reconciled cutover described in the database playbook below. Two independent writes are not a migration consistency mechanism.
Phase five: decommission. Retain the components and change evidence required by the agreed recovery window. Remove them only after outstanding work is reconciled, the replacement has met its operating criteria, and the team has a tested recovery path. Read-only old data is useful evidence; it may be too stale to resume serving writes.
Evaluate each phase against its own baseline. Security controls, transfer costs and request latency require different evidence; none improves automatically because the endpoint moved. Stop at the phase that meets the product's needs when further migration cannot justify its cost.
Comparing migration approaches across platforms
Migration tooling reveals a platform's architectural assumptions. Cloudflare's approach differs from hyperscaler migration paths in ways that reflect the platform's edge-native design.
Hyperscaler migrations typically involve lift-and-shift tooling designed to move workloads between similar environments. AWS Migration Hub, Azure Migrate, and Google Cloud's migration tools assume you're moving VMs, containers, or databases from one data centre to another. The workload structure stays the same; only the infrastructure underneath changes. This works well for homogeneous migrations (on-premise to cloud, cloud to cloud) but provides little help when the target platform has a fundamentally different execution model.
Cloudflare provides no equivalent lift-and-shift tooling because the concept doesn't apply. You cannot lift a Lambda function and shift it to Workers without understanding how the execution model differs. Instead, Cloudflare provides infrastructure-level migration tools (Sippy, Super Slurper for storage; Hyperdrive for database connectivity; Tunnel for network bridging) that handle the infrastructure layer while you handle the architectural translation.
This difference is honest about the work involved. A Lambda-to-Workers migration is not a configuration change; it's an architectural decision with implications for memory management, execution time, global distribution, and state coordination. Tools that pretend otherwise create false confidence. Cloudflare's tooling handles what can be automated (copying objects, pooling connections, routing traffic) and leaves the architectural decisions where they belong: with the engineering team.
The tradeoff is real. Hyperscaler-to-hyperscaler migrations can be faster for workloads that translate directly. Cloudflare migrations require more thought but produce architectures that exploit the platform's strengths rather than merely reproducing what existed before.
The playbooks that follow aim to preserve service through coexistence and controlled cutover. Each identifies the state that must remain correct and the evidence needed before retiring the old path. A brief writer pause can be the safer choice when the available change-capture or ownership mechanism cannot support an uninterrupted handoff.
Playbook: S3 to R2
Object storage migration is conceptually simple (copy files from one bucket to another), but it becomes operationally complex at scale with billions of objects, petabytes of data, and applications expecting zero downtime. R2 provides two migration tools for different scenarios.
Super slurper: complete migration
Super Slurper copies all objects from a source bucket to R2 in a single migration job. Use it when you want everything migrated, can tolerate egress costs, and need migration completed within a predictable timeframe.
Configure Super Slurper through the Cloudflare dashboard: specify source credentials, source bucket, destination R2 bucket, and optional path filters. It handles parallelisation, retries, and progress reporting. Migration that would take weeks with sequential copying completes in hours or days.
Super Slurper supports S3-compatible sources beyond AWS: Google Cloud Storage, MinIO, Backblaze B2, Wasabi, DigitalOcean Spaces.
The process:
- Create destination R2 bucket
- Configure Super Slurper with source credentials and bucket details
- Optionally configure path filters for specific prefixes
- Start migration and monitor progress
- Validate migrated objects match source (spot-check counts and checksums)
- Update application configuration to point to R2
- Maintain source bucket during validation period (weeks, not days)
- Delete source bucket after confidence is established
Calculate egress costs before committing to a strategy. AWS charges $0.09/GB for S3 egress; migrating 10 TB costs $900 in egress fees alone, potentially more than several months of R2 storage. For massive buckets, Sippy's on-demand migration may prove more economical than Super Slurper's complete copy.
Sippy: incremental migration
Sippy migrates objects on demand. Configure R2 as a caching layer in front of your source bucket. When an object is requested from R2 and doesn't exist, Sippy fetches it from the source, stores it in R2, and returns it. Frequently accessed objects migrate first; rarely accessed objects migrate only when needed.
The benefit is economics. You pay egress only for objects actually requested, and frequently requested objects incur egress only once. For buckets where 10% of objects receive 90% of requests, Sippy can reduce migration egress costs by an order of magnitude.
The tradeoff is timeline. Migration completes only when all objects have been requested, which might be never. Objects never accessed remain in the source bucket indefinitely, continuing to incur storage costs.
Use Sippy when:
- Egress costs for complete migration are prohibitive
- You want to serve frequently accessed content from R2 immediately
- Complete migration isn't required
- Most objects are rarely requested
The process:
- Create destination R2 bucket
- Configure Sippy with source bucket credentials
- Update application to request from R2 (Sippy handles fallback transparently)
- Monitor migration progress as objects copy on access
- Optionally run Super Slurper with "skip existing" to complete remaining objects
Combined strategy: Enable Sippy first to immediately serve frequently accessed objects from R2. After access patterns stabilise, run Super Slurper to migrate the long tail. This minimises egress costs while ensuring complete migration.
After migration: compatibility notes
R2 is S3-compatible, but compatibility isn't identity. Test thoroughly. Common differences:
ETags may differ. R2's ETag calculation matches S3 for single-part uploads, but Sippy may migrate multipart objects with different part sizes, producing different ETags. Applications validating ETags across migration will see mismatches.
Some S3 features aren't supported. S3 Object Lock, S3 Select, Requester Pays, and certain storage classes don't exist in R2. Check the compatibility matrix; missing features require application changes.
Endpoint URLs change. S3 endpoints follow bucket-name.s3.region.amazonaws.com. R2 follows account-id.r2.cloudflarestorage.com or custom domains. Update application configuration and SDK initialisations.
IAM policies don't transfer. R2 uses API tokens with specific permissions. Recreate your access control model using R2's authentication mechanisms.
Keeping reads and writes correct during cutover
Sippy can keep reads of uncopied objects available while traffic moves to R2. It is not continuous synchronisation: a source object changed after copying does not update the R2 copy, and deleting only the R2 copy can let a later read fetch the source object again.
Choose a write authority for the transition. Route all writers to R2 at cutover, or provide a tested mechanism to reconcile mutations and deletions until they move. Inventory background jobs and direct bucket clients as well as the main application. Test compatibility before changing endpoints and credentials.
Use Super Slurper to copy the remaining inventory with an overwrite policy appropriate to that write authority. Verify skipped objects and content, not only the job's completion status, then disable Sippy when the destination is complete.
Keeping S3 preserves the old copy, but it is not sufficient rollback after R2 has accepted new writes. A usable rollback needs those writes and deletions reconciled back to the source, or an explicit decision to pause writes while recovering. Measure the delivery path separately: S3 API compatibility does not itself make objects cached at every edge location.
Playbook: Lambda to Workers
Moving serverless compute from Lambda to Workers requires understanding model differences, not just translating syntax.
The model differences
Geographic distribution: Lambda functions are regional (deploy to us-east-1, execute in us-east-1; global distribution requires deploying to multiple regions and managing routing through Route 53, CloudFront, or API Gateway). Workers deploy globally by default; request routing, capacity and placement configuration determine where each invocation executes.
Resource limits: Lambda supports up to 10 GB memory and 15-minute execution. Workers have 128 MB memory and 30 seconds of CPU time for HTTP handlers by default, configurable up to 5 minutes. Cron Triggers with hourly or longer intervals get 15 minutes of CPU time. Queue consumers get up to 15 minutes of wall time but the same 5-minute CPU time ceiling as other Workers. This is the most common migration blocker.
Cold starts: Workers’ isolate model reduces runtime startup overhead, but application initialisation and the first dependency call still affect latency. Compare cold and warm requests using the actual runtime, packages and data path. Lambda provisioned concurrency and scheduled warming do not map directly to Workers lifecycle controls.
Networking: Lambda connects to VPCs natively through ENI attachment. Workers connect to private resources through Cloudflare Tunnel or VPC Services integration.
Pricing: Lambda charges for GB-seconds (memory multiplied by wall-clock time). Workers charge for CPU time, with I/O wait free. Lambda charges while your function waits for a database response; Workers don't. I/O-heavy workloads typically cost less on Workers.
Assessment questions
Before migrating any Lambda function, answer these questions:
Does it fit Workers' constraints? If the function uses more than 128 MB memory, Workers isn't the right target without architectural changes. If it needs more than five minutes of CPU time, split the work or choose a different runtime. Long waits for I/O are a separate question: HTTP execution depends on the client connection, while background work needs a durable trigger.
What does it access? If the function accesses VPC resources (RDS, ElastiCache, internal services), how will Workers reach them? Cloudflare Tunnel works but adds latency and complexity. If using DynamoDB, will you migrate to D1, use Hyperdrive with an external database, or accept cross-cloud latency?
How is it triggered? HTTP triggers translate directly to Workers fetch handlers. SQS triggers require migrating to Cloudflare Queues or maintaining SQS with a polling Worker. EventBridge, Step Functions, and other AWS-specific triggers require alternative architectures.
What libraries does it use? Some npm packages assume capabilities Workers do not provide: persistent or host filesystem access, child processes, or native binary addons. A dependency using bundled files or temporary storage may work through Node.js compatibility. Test dependencies in the Workers environment before committing.
What's the quantified benefit? "Lower latency" isn't a benefit; "reducing p95 latency from 180ms to 45ms for European users" is. If you can't quantify expected improvement, you can't evaluate whether migration succeeded.
"Lower latency" and "reduced costs" aren't quantified benefits. Without baseline metrics and continuous measurement, you can't know if migration succeeded. If you can't articulate expected improvement in numbers, question whether you should migrate.
Migration process
The process below retains a valid old handler while the replacement is tested. Preserve that handler's access to compatible state and make request ownership explicit. Whether cutover can avoid a writer pause depends on those contracts.
For functions that pass assessment:
1. Translate the handler. Lambda handlers receive event and context objects with AWS-specific structure. Workers handlers receive standard Request objects and env for bindings. The translation is mechanical but requires attention to input parsing and output formatting.
// Lambda
export const handler = async (event, context) => {
const body = JSON.parse(event.body);
const userId = event.pathParameters.userId;
// ... business logic ...
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
};
};
// Workers
export default {
async fetch(request, env) {
const body = await request.json();
const url = new URL(request.url);
const userId = url.pathname.split('/')[2]; // or use a router
// ... business logic (largely unchanged) ...
return Response.json(result);
}
};
2. Replace service integrations deliberately. S3 object access can become R2 bindings and SQS publishing can become Queue bindings after their semantics are checked. DynamoDB can remain behind its API; moving to D1 or PostgreSQL/MySQL through Hyperdrive also requires a data-model and state migration. Hyperdrive is connectivity to a supported relational database, not a DynamoDB replacement. Apply the database playbook below.
3. Configure bindings. Resources IAM-attached in Lambda are binding-attached in Workers. Add appropriate bindings to wrangler.jsonc.
4. Test locally. Use wrangler dev to verify behaviour against realistic inputs including edge cases from production logs.
5. Deploy to preview. Test with real Cloudflare infrastructure but without production traffic. Verify integrations, latency, and error handling.
6. Route incrementally. Use gradual deployments to increase exposure only after the agreed correctness, latency and error criteria pass. Observe representative traffic, including scheduled work and time-zone-dependent paths. Allocation changes within Cloudflare avoid relying on DNS cache expiry. Workers VPC Services can provide private connectivity during coexistence; evaluate Smart Placement against the measured backend path.
7. Monitor and compare. Compare latency distributions, error rates, and costs between versions. Geographic distribution changes latency patterns in expected ways: a Lambda function in us-east-1 serving a European user at 180ms might become a Worker serving from Frankfurt at 15ms, but the same user's requests to a backend in us-east-1 might show similar total latency until data migration completes. Understand which differences are improvements, which are expected consequences of the new architecture, and which indicate bugs.
8. Complete cutover. When metrics confirm the Worker performs as well or better across all traffic stages, route 100% to Workers. Retain the old handler only as part of a tested recovery path that accounts for current state and writes accepted after cutover.
9. Decommission. Retire Lambda after the agreed observation and recovery window, with outstanding work reconciled and a tested replacement recovery path. Retain code, configuration and state evidence needed by that plan; elapsed weeks alone do not prove readiness.
What the economics look like after migration
Lambda charges for GB-seconds, which is memory allocation multiplied by wall-clock execution time. A function configured at 1 GB running for 500ms costs 0.5 GB-seconds regardless of whether the function spent 490ms waiting for a database response. Workers charge only for CPU milliseconds, the time your code actually executes on a processor rather than wall time. The same function, rewritten as a Worker, might consume 10ms of CPU time while waiting 490ms for a database response, and you pay for 10ms instead of 500ms.
For I/O-heavy workloads, estimate both serial and parallel calls. Five independent 200ms API calls can overlap; five dependent calls cannot. Lambda bills the elapsed execution duration, while Workers bills the CPU used to orchestrate it. Compare the resulting request and execution charges, not the raw millisecond counts.
Lambda's API Gateway adds $3.50 per million requests for REST APIs ($1.00 for HTTP APIs) on top of compute costs. Workers include HTTP handling in the base pricing; no separate API Gateway charge applies. For an API handling 50 million monthly requests through a REST API, this single line item accounts for $175 per month that disappears after migration.
The gap narrows for compute-intensive workloads. A function spending most of its execution time on computation rather than I/O pays for that CPU time on both platforms. Workers' per-CPU-millisecond pricing exceeds Lambda's GB-second pricing for sustained computation, particularly when Lambda functions are configured with higher memory allocations that include proportionally more CPU power. Model your specific workload rather than assuming Workers are always cheaper; the pricing advantage is architectural, not universal.
Playbook: Vercel/Netlify to Workers
Vercel and Netlify provide managed deployment for frameworks like Next.js, SvelteKit, and Astro. Migration to Cloudflare means moving from managed platform to managed infrastructure with more control, more configuration, and different trade-offs.
What changes
Deployment model: Vercel and Netlify infer configuration from your framework (push to git and deployment happens), whereas Cloudflare requires explicit wrangler.jsonc configuration. The magic is replaced with configuration files you control.
Environment handling: Vercel's environment variables are managed through their dashboard. Cloudflare uses Wrangler secrets for sensitive values and wrangler.jsonc vars for non-sensitive configuration.
Serverless functions: Vercel Functions and Netlify Functions have their own conventions: file-based routing, specific handler signatures. These become Workers with explicit routing. Translation is straightforward but requires touching every function.
Edge functions: Vercel and Netlify Edge Functions are conceptually similar to Workers but have API differences. The concepts transfer; the syntax doesn't.
Data services: A Redis-compatible service such as Vercel KV does not map directly to Workers KV. Follow the Redis playbook below to separate caching, sessions, counters and pub/sub. Existing PostgreSQL can remain behind Hyperdrive or move to D1 if its SQL and partition requirements fit. Assess object API compatibility before moving blobs to R2.
The process
1. Choose your framework's Cloudflare path:
- Next.js: assess the current OpenNext adapter and its supported runtime features
- React Router v7 (Remix): Official Cloudflare Vite plugin
- SvelteKit:
@sveltejs/adapter-cloudflare - Astro:
@astrojs/cloudflare - Nuxt: Nitro
cloudflarepreset
2. Create Cloudflare resources. Before migrating code, create needed infrastructure: KV namespaces, D1 databases or Hyperdrive connections, R2 buckets, Queues.
3. Configure wrangler.jsonc. Define bindings, build commands, and output configuration. This replaces Vercel's implicit configuration.
{
"name": "my-app",
"compatibility_date": "2025-01-15",
"compatibility_flags": ["nodejs_compat"],
"build": {
"command": "npm run build"
},
"kv_namespaces": [
{ "binding": "CACHE", "id": "your-kv-namespace-id" }
],
"d1_databases": [
{ "binding": "DB", "database_name": "my-app-db", "database_id": "your-d1-database-id" }
]
}
4. Adapt environment variables. Move secrets using wrangler secret put. Move non-secret configuration to wrangler.jsonc vars or .dev.vars.
5. Adapt API routes and middleware. Vercel API routes become Workers handlers. Request/response model is similar but not identical.
6. Test thoroughly. Deploy to a preview URL. Test every route, API endpoint, authentication flow, edge case. Framework adapters handle most translation, but subtle differences surface in testing.
7. Configure DNS. Use an active Cloudflare zone with a Worker Custom Domain or route, as appropriate, or begin on a workers.dev subdomain. Moving DNS management to Cloudflare does not require transferring the domain’s registrar.
8. Compare metrics. Run Cloudflare alongside Vercel. Compare Core Web Vitals, time to first byte, error rates. Investigate differences before completing migration.
Reasons not to migrate
Migration from Vercel or Netlify isn't always beneficial. Consider staying if:
Framework integration is tighter elsewhere. Some Vercel features (automatic ISR, preview deployments with database branching, integrated analytics) don't have direct Cloudflare equivalents. If these are central to your workflow, migration removes capabilities you depend on.
Managed simplicity has value. Vercel and Netlify excel at removing decisions. Cloudflare provides more control but requires more configuration. If your team values the managed experience and the current platform works well, migration adds operational burden without proportionate benefit.
Cost comparison favours the current platform. Don't assume Cloudflare is cheaper. Compare actual costs at your traffic levels, including engineering time for migration.
Migration effort exceeds benefit timeline. If migration takes three months and you're uncertain about the product's future beyond six months, payback may not justify investment.
Migrate when Cloudflare provides specific advantages you need: lower latency through global distribution, Durable Objects for coordination, Workers AI for inference, or demonstrated cost savings at your scale. Don't migrate because it feels like progress.
Playbook: Containers to Cloudflare
Organisations running containers on ECS, Kubernetes, or other orchestration platforms have two migration targets: Workers serve workloads that fit the isolate model, while Cloudflare Containers serve workloads that need container capabilities.
Deciding the target
Most containerised workloads can run as Workers. Containers often exist because the team knew containers, not because the workload required them.
Migrate to Workers when:
- Memory usage stays under 128 MB
- Execution completes within time limits
- Dependencies run in the Workers runtime: JavaScript, TypeScript, WebAssembly, or compatible Python packages
- No filesystem persistence required between requests
Migrate to Cloudflare Containers when:
- Memory requirements exceed 128 MB
- The workload needs a conventional filesystem beyond bundled files and request-scoped temporary storage
- Dependencies include native binaries that won't compile to WebAssembly
- The runtime or native dependencies are unsupported by Workers, such as a JVM service or Python extension without a compatible WebAssembly build
Python services deserve the same assessment as JavaScript services. FastAPI, Django, and Flask can retain their framework structure on Python Workers, and Hyperdrive allows them to keep PostgreSQL or MySQL. Check package compatibility, startup latency, memory use, and driver behaviour before choosing Containers solely to preserve Python. Chapter 3 explains those runtime boundaries.
Workers should be the default assumption. Containers are for workloads that don't fit.
Container migration process
For workloads targeting Cloudflare Containers:
1. Measure and reduce startup time. Test the actual image, dependencies and first useful operation. Minimise image size with slim base images and multi-stage builds, and defer initialisation that the first request does not need. Use those measurements to choose the sleep and warm-capacity policy.
2. Configure the container in wrangler.jsonc:
{
"containers": [
{
"class_name": "MyContainer",
"image": "./Dockerfile",
"max_instances": 10
}
]
}
3. Create the routing Worker. Workers route requests to Containers, deciding which requests need container processing and which can be handled at the edge.
4. Handle the cold start experience. If measured startup exceeds the user’s waiting budget, show a pending state, process asynchronously or keep capacity warm where justified. Do not present an operation as accepted until its durable handoff has succeeded.
5. Deploy and test with realistic traffic patterns, paying attention to cold start frequency and duration.
Containers on Cloudflare serve workloads needing container capabilities deployed globally with Cloudflare's network benefits. They're not a general container platform.
Playbook: database migration
Compute migration often triggers database migration because Workers connecting to RDS in us-east-1 inherit that latency regardless of where the Worker runs. But database migration is not always the answer, and when it is, the target depends on your data model and access patterns.
The fundamental decision: migrate or connect
Before planning how to migrate data, decide whether to migrate it at all. Consider these factors:
Use Hyperdrive (keep your existing database) when:
- Your PostgreSQL or MySQL database works well and you've invested in its schema, indexes, and operational tooling
- Data volume exceeds D1's 10 GB limit per database and sharding adds unwanted complexity
- Your application relies on PostgreSQL-specific features: advanced JSON operators, full-text search with ranking, PostGIS spatial queries, stored procedures, or triggers
- Regulatory requirements mandate specific database platforms or locations
- The database serves multiple applications, not just the workload you're migrating
Hyperdrive provides connection pooling, prepared statement caching, and global connection reuse. Workers connect through Hyperdrive; the database stays where it is. This is a valid permanent architecture, not a migration stepping stone.
Migrate to D1 when:
- A D1 partition fits the data and its measured primary/replica path meets latency requirements
- Your data model fits SQLite's capabilities
- Total data per logical database stays under 10 GB
- You're building new applications or rebuilding existing ones
- Horizontal partitioning (database per tenant, per region, or per entity type) matches your domain
Migrate to KV when:
- Data is key-value shaped with simple access patterns
- Cached, eventually consistent reads fit the application, including propagation that can exceed 60 seconds
- You're replacing DynamoDB, Redis, or similar stores used primarily for caching or simple lookups
Schema translation: PostgreSQL and MySQL to D1
D1 runs SQLite. Most SQL translates directly, but PostgreSQL and MySQL features that don't exist in SQLite require application changes.
Data types that need translation:
| PostgreSQL/MySQL | SQLite/D1 | Notes |
|---|---|---|
| SERIAL, AUTO_INCREMENT | INTEGER PRIMARY KEY | SQLite auto-increments INTEGER PRIMARY KEY automatically |
| BOOLEAN | INTEGER | Use 0 and 1; SQLite has no native boolean |
| TIMESTAMP WITH TIME ZONE | TEXT or INTEGER | Store as ISO 8601 strings or Unix timestamps |
| JSON, JSONB | TEXT | SQLite stores JSON as text; use json_extract() for queries |
| ARRAY | TEXT or separate table | No native arrays; serialise or normalise |
| UUID | TEXT or BLOB | Store as string or 16-byte blob |
| ENUM | TEXT with CHECK constraint | No native enums |
| DECIMAL, NUMERIC | REAL or TEXT | REAL for calculations; TEXT for exact decimal preservation |
Features that don't translate:
SQLite supports triggers and many SQL functions, but PostgreSQL or MySQL stored procedures and engine-specific functions do not translate automatically. Inventory the database-resident logic, adapt supported SQL deliberately and move the remainder into application code where appropriate. Preserve its transaction and failure behaviour during the move.
Foreign key constraints exist in SQLite but are disabled by default. D1 enables them, but behaviour differs subtly from PostgreSQL. Test constraint violations explicitly; don't assume identical behaviour.
Full-text search exists in SQLite via FTS5, but the syntax and ranking differ from PostgreSQL's tsvector/tsquery. If search quality matters, consider whether D1's FTS meets requirements or whether Vectorize with semantic search better serves the use case.
D1's horizontal model
D1’s paid per-database limit is 10 GB. Design for that boundary rather than assuming it will rise. Independent tenant or domain partitions can fit well; keep an external database when required transactions and joins span a larger shared dataset.
When to shard from the start:
If your current database exceeds 10 GB, or will exceed it within a year, plan your D1 architecture around multiple databases. Common patterns:
-
Database per tenant for multi-tenant SaaS. Each customer's data lives in isolation. The 10 GB limit applies per tenant, not globally. Cross-tenant queries become impossible, which is often a feature for data isolation.
-
Database per entity type when different data has different access patterns. Users in one database, transactions in another, audit logs in a third. Each can scale independently.
-
Database per time period for append-heavy data. Current month in one database, archives in others. Query routing adds complexity but prevents unbounded growth.
Routing queries to the right database:
Sharded architectures need routing logic. A Worker determines which database handles each request based on tenant ID, entity type, or date range. This logic is straightforward but must be consistent; routing errors corrupt data.
function getDatabaseForTenant(env: Env, tenantId: string): D1Database {
// Bindings named DB_TENANT_1, DB_TENANT_2, etc.
const dbName = `DB_TENANT_${tenantId}`;
return env[dbName] as D1Database;
}
Validate the authenticated tenant against a trusted mapping before selecting a binding. For larger fleets, compare deployment-managed bindings and routing Workers with authenticated D1 REST API calls. A database ID is not a runtime binding: the REST path introduces API credentials, rate limits and a different latency path. Protect the registry and any cached mappings according to the access risk described in Chapter 25.
Data migration process
Export and translate the schema first, then test every query path against an empty D1 database. Data-type conversion and SQL syntax are only part of compatibility: check uniqueness, constraints, triggers, transaction boundaries and numeric precision.
Import a representative dataset and compare counts, checksums and application results. D1 imports SQL through Wrangler; use a SQL-aware export or batching process that preserves complete statements. Splitting by line count can break statements, and production imports must explicitly target the remote database.
For the live transfer, use the snapshot, change-capture and cutover procedure below. A successful bulk import proves neither that concurrent writes were captured nor that rolling back will preserve writes made after cutover.
DynamoDB to Cloudflare
DynamoDB's key-value and document model maps to either KV or D1, depending on how you use it.
DynamoDB as key-value store → KV:
If you use DynamoDB primarily for simple get/put operations with partition keys, KV is the natural target. Access patterns translate directly:
| DynamoDB | Workers KV |
|---|---|
| GetItem | kv.get(key) |
| PutItem | kv.put(key, value) |
| DeleteItem | kv.delete(key) |
KV caches can serve an older value while changes propagate, which can take 60 seconds or longer. This differs from DynamoDB's strongly consistent read option. If the application depends on current reads, choose an authoritative store and read path rather than assuming KV will meet a fixed staleness bound.
DynamoDB with complex queries → D1:
If you use DynamoDB's Query and Scan operations with sort keys, filters, and secondary indexes, D1's SQL model may actually simplify your code. GSIs and LSIs exist because DynamoDB's query model is limited; SQL handles the same access patterns natively.
Translate DynamoDB's single-table design back to normalised tables. The patterns that optimise DynamoDB access (composite keys, overloaded attributes, sparse indexes) don't apply to SQL databases and make schemas harder to understand.
DynamoDB Streams → Queues:
If you use DynamoDB Streams for change data capture, Workers don't have a direct equivalent for D1. Options include:
- Transactional outbox: commit the mutation and its event in D1 together, then publish from a retryable dispatcher. Preserve event identity, ordering information and deletion records. Chapters 9 and 23 explain the handoff and recovery.
- Polling: use a durable change log and cursor when every change matters. Scanning current rows by timestamp can miss deletions and intermediate updates; reserve that shortcut for projections where periodic reconciliation is sufficient.
- External CDC: if the source remains DynamoDB during transition, process streams in Lambda and forward to Cloudflare with checkpointing and duplicate handling.
When not to migrate data
Database migration adds schema translation, data transformation, change capture, reconciliation and cutover work beyond the compute migration. Estimate those tasks separately before deciding that moving the data is necessary.
Don't migrate data when:
Hyperdrive solves the latency problem. If your concern is Workers connecting to a distant database, Hyperdrive's connection pooling and caching may provide sufficient improvement without migration risk.
The database serves multiple applications. Migrating data used by systems outside your control creates coordination overhead that rarely justifies the benefit.
You're uncertain about D1's fit. Prototype with Hyperdrive first. If access patterns prove D1-compatible and the migration benefit becomes clear, migrate then. Premature data migration creates rollback complexity that compute migration doesn't.
Compliance requires specific platforms. Some regulations mandate specific database technologies or certifications. Verify D1's compliance status before assuming you can migrate.
Keeping a live database migration consistent
Move compute first when practical. Workers can continue reading and writing the existing PostgreSQL or MySQL database through Hyperdrive, allowing the compute change to be evaluated independently of a data migration. Keeping that database is also a valid permanent design.
If D1 is the destination, start with one authoritative writer. Two independent writes, one to each database, can partially succeed. Use the source's transaction log or an outbox committed with the source mutation to produce a durable, replayable change stream. Include every writer, including jobs and administrative tools.
Take a consistent snapshot with a known change-stream position, load it into D1, then replay changes after that position in the required order. Alternatively, use version checks that stop older backfill rows overwriting newer target values. Track deletions and schema changes as well as inserts and updates. Retries must be safe.
Compare row counts, content and application results while the target catches up. A period of apparently healthy dual writes is not proof of completeness: establish a reconciled position through which all changes have arrived. Shadow reads can reveal query differences without making a stale target authoritative for user decisions.
At cutover, fence or briefly pause the old writer, apply the remaining changes and direct writes to the new authority. Whether this can happen without a visible pause depends on the application's consistency requirements and migration mechanism. Do not replace that decision with an arbitrary traffic percentage.
Define rollback before the first authoritative D1 write. The old database must receive subsequent mutations, or rollback requires a reconciliation and write pause. Retaining the old database and code alone does not preserve new data. Remove the change-capture path only after the agreed rollback window and final reconciliation.
Playbook: Redis and ElastiCache to KV and Durable Objects
Redis serves as caching layer, session store, rate limiter, pub/sub broker, and general-purpose coordination tool across most hyperscaler architectures. No single Cloudflare product replaces all of these roles. The migration target depends on which role Redis plays in your system.
Choosing the target
Redis usage falls into distinct categories, each mapping to a different Cloudflare primitive. Most production Redis instances serve multiple categories simultaneously, which means migration involves decomposing Redis into separate concerns handled by separate products.
Caching can map to KV when its staleness fits. Query results, API responses and computed values are suitable candidates when they can be refreshed safely. KV updates can take 60 seconds or longer to become visible elsewhere, and cache settings may extend that window. Define invalidation and the consequence of serving old data before treating Redis and KV as interchangeable caches.
Session storage depends on the required freshness and operations. KV can hold values whose staleness is acceptable, such as a displayed cart count. D1 with an appropriate read path and transactional updates can hold current session state. Choose a Durable Object when the session also needs an active owner coordinating turns or connections. Revocation and payment decisions need their own current authority.
Rate limiting and counters need an atomic decision. A Durable Object can own a quota and its coordination logic; a conditional D1 update can enforce a counter or reservation within one database. Choose from the invariant, request rate and latency requirement. A separate KV read and write cannot protect the increment: concurrent callers can overwrite each other’s result.
Pub/sub maps to Queues or Durable Objects with WebSockets. Redis pub/sub for fan-out messaging translates to Queues for asynchronous distribution or Durable Objects with WebSocket hibernation for real-time broadcast. Neither is a direct replacement; the programming model changes.
The model differences
Redis operates as a single (or clustered) in-memory store with sub-millisecond latency from co-located clients. KV serves cached values through a distributed hierarchy; a miss may reach its central stores. It provides eventual consistency and permits at most one write per second per key. Measure hot and cold reads from the application’s execution locations. These are fundamentally different performance profiles, and treating KV as "Redis but distributed" leads to architectural mistakes.
The write rate limit deserves emphasis. Redis handles thousands of writes per second to a single key. KV limits writes to one per second per key, returning 429 errors when exceeded. Applications updating the same key frequently (hit counters, real-time dashboards, high-frequency session updates) cannot use KV without architectural changes. Either distribute writes across multiple keys and aggregate on read, or use Durable Objects for write-heavy state.
Durable Objects provide one authority for a state boundary, with finite throughput determined by CPU work, storage and contention. They do not provide unlimited write rates. A rate limiter can make an atomic decision in an object, but remote callers pay the network round trip to that object; compare placement and contention as well as consistency.
Migration process
1. Audit Redis usage patterns. Categorise every Redis operation in your codebase: pure caching, session reads, session writes, counters, rate limiting, pub/sub, sorted sets, Lua scripts. Each category migrates differently.
2. Migrate caching first. Caching is the lowest-risk migration because cache misses are handled by design. Deploy Workers that check KV before Redis, writing to KV on cache miss. Over time, KV absorbs the read load while Redis handles decreasing traffic. Since cache data is regenerable, there is no data migration; you simply let KV populate organically.
3. Preserve one session authority. Keep Redis authoritative while durable versioned changes populate the replacement, including renewals, expiry and revocation. Reconcile through a known position, fence the old writer and transfer authority under the chosen freshness policy. If the current session system cannot supply a reliable change history, an explicit reauthentication cutover may be simpler and safer.
4. Migrate coordination patterns last. Rate limiters, counters, and leases may benefit from Durable Objects and represent the most significant architectural change. Implement new rate limiting in Durable Objects alongside existing Redis rate limiters. Compare decisions between both systems before trusting Durable Objects alone.
Zero-downtime caching migration
Disposable caches can populate on demand while the authoritative source continues serving misses. Check that source capacity can absorb cold traffic; a cache miss is only harmless when the fallback remains available.
Deploy a Worker that reads from KV first. On cache hit, return immediately. On cache miss, read from the authoritative source (database, API, Redis if it is also caching), write the result to KV asynchronously using ctx.waitUntil(), and return. This pattern means KV starts empty and fills organically based on real traffic. Frequently accessed data migrates first; rarely accessed data migrates on demand. No bulk data copy required.
Sessions follow the authority-transfer procedure above, not ordinary cache warming. Two independent writes can disagree, including about a revoked session. Preserve the recovery position and test logout during cutover before promising uninterrupted sessions.
Rate limiting needs a defined overlap policy too. During transition, consulting both limiters and enforcing the more restrictive result can prevent either from granting additional capacity, provided every request reaches both and their unavailable-state behaviour is explicit. Expect more restrictive limits during the overlap and test the handoff.
Playbook: SQS, SNS, and Service Bus to Queues
Queue migration must preserve accepted work and prevent duplicate business effects. Both delivery systems and the handover can retry messages. Plan for replay and in-flight work rather than promising that every message reaches exactly one consumer.
The model differences
SQS consumers poll, process and acknowledge messages by deleting them. Visibility timeouts temporarily hide in-flight messages. FIFO queues add message-group ordering and deduplication, but consumers still need safe handling of retries and failures after a business effect has occurred.
Cloudflare Queues uses a push model by default: Cloudflare invokes your Worker's queue() handler with batches of messages. The Worker processes the batch and returns; successful return acknowledges all messages. Individual messages can be retried by calling message.retry(). Pull-based consumption is also available for consumers outside the Workers ecosystem.
The differences that affect migration planning are significant. Queues lacks FIFO ordering guarantees and message deduplication. If your SQS usage relies on either of these, you need application-level workarounds. Queues does support dead letter queues natively: messages that fail after a configurable number of retries (up to 100) route to a designated DLQ rather than being discarded. This aligns with the SQS pattern, though the configuration differs.
Message size also differs: SQS supports up to 1 MiB per message (or larger with S3 offloading via the Extended Client Library), while Queues supports 128 KB. Messages exceeding 128 KB need a claim-check pattern: store the payload in R2 and send the R2 key as the message body.
Handling missing features
Message ordering. Queues does not guarantee FIFO order. If the domain requires a sequence per entity, include sequence numbers assigned by an authoritative producer and define how to handle gaps and replay. A Durable Object can own that ordered state, but merely serialising messages in their arrival order does not reconstruct the original sequence.
Deduplication. Give each business operation a stable identifier and make the effect idempotent. Commit a local effect with its receipt atomically, or use the external receiver's idempotency mechanism. An eventually consistent KV lookup or a separate check before acting cannot guarantee duplicate suppression under concurrency. Chapter 24 covers the failure window.
Fan-out (SNS equivalent). SQS paired with SNS provides topic-based fan-out: one message published to a topic reaches multiple queues. Cloudflare Queues has no native topic/subscription model. Implement fan-out in the publishing Worker: when a message needs multiple consumers, publish to multiple Queues explicitly. This is more code but provides explicit control over which consumers receive which messages.
Migration process
1. Inventory the contract. Record throughput, payload sizes, ordering requirements, retries, retention, dead-letter handling and the business effect of each message. Choose a stable operation identifier that survives either transport.
2. Validate the replacement consumer. Test duplicates, delayed messages, crashes after an external effect and poison messages. Run shadow traffic without production side effects. Recording and acknowledging a shadow receipt consumes that copy; it does not leave work waiting for later activation.
3. Make publication recoverable. If publishing to both systems for comparison, record the intended deliveries durably and retry each independently. Two sequential publish calls are not atomic, even when placed behind one intermediary Worker. Compare identifiers and terminal outcomes rather than counts alone.
4. Transfer ownership explicitly. Establish a cutover position or epoch. Let the old consumer finish the work it owns while routing new work to the replacement, with idempotency covering any overlap. Track in-flight messages, retries and dead-letter queues until every accepted operation has an accountable outcome.
5. Rehearse rollback and retire deliberately. Preserve a replay source for the agreed recovery window and verify that its retention is long enough. Expiring messages is deletion, not successful draining. Decommission the old queue only after reconciling outstanding work and confirming where replay would come from.
Asynchronous processing can absorb a short handover delay. It does not by itself prove no loss or duplicate effects; those properties come from durable publication, explicit ownership and idempotent consumers.
Playbook: Step Functions and Durable Functions to Workflows
Workflow orchestration migration is distinctive because orchestrators manage long-running processes that may span hours or days. You cannot simply cut over mid-execution; running Step Function state machines must complete on the old system while new executions start on the new one.
The model differences
Step Functions uses a declarative state machine model: you define states, transitions, and error handling in JSON (Amazon States Language). Each state is a node in a graph; execution follows edges between nodes. The model is visual, which aids understanding but limits expressiveness for complex conditional logic.
Cloudflare Workflows uses an imperative code model: you write a TypeScript class with a run() method containing sequential steps. Each step.do() call persists its result; if the Workflow fails and restarts, completed steps return their persisted results without re-executing. This model handles complex branching, loops, and dynamic logic naturally because it's just code, but loses the visual clarity of state machine diagrams.
Compare the workflow type as well as the vendor. Step Functions Standard bills state transitions; Express has a different duration and request model, a five-minute execution limit, and different execution guarantees for synchronous and asynchronous calls. Cloudflare Workflows bills steps and retained state alongside CPU time and requests. Model the actual waits, retries and retention using Chapter 8, then compare the failure semantics the application relies on.
Step Functions integrates deeply with AWS services through native integrations (invoke Lambda, read from DynamoDB, send to SQS, all without custom code). Workflows integrates with Cloudflare services through bindings and with external services through fetch(). If your Step Functions workflow orchestrates exclusively AWS services, migration requires replacing native integrations with explicit API calls.
Assessment before migration
Map execution semantics before syntax. Identify which branches have independent checkpoints, what is retried after failure, and where side effects occur. Grouping several calls inside one step.do() makes them share a retry boundary; Promise.all() alone does not preserve separate branch checkpoints.
Check execution limits. Count the steps and retained results in the longest paths, including loops and retries, against the current plan and configured limits in Chapter 8. Sleep steps do not count towards the execution step limit, which is distinct from the billing meter.
Evaluate wait patterns. Step Functions' Wait states pause execution for a specified time. Workflows' step.sleep() and step.sleepUntil() provide equivalent capability. Step Functions' Callback pattern (wait for external token) maps to step.waitForEvent(), which pauses the Workflow until an external system sends a named event.
Identify running executions. Before migration, understand how many Step Functions executions are active and their expected completion times. These must run to completion on the old system; they cannot be transferred mid-execution to Workflows.
Migration process
1. Translate state machines to Workflow classes. Task states become step.do() calls, waits become durable sleeps, and choices become conditional logic. Preserve independent checkpoints for parallel effects that must retry separately. Grouping concurrent calls in one step makes them share a retry boundary, so any effect already completed must tolerate repetition or be reconciled. Map states need bounded fan-out rather than an unbounded generated loop.
2. Handle AWS-native integrations. Step Functions' direct integrations with DynamoDB, SQS, SNS, and other AWS services become explicit API calls in Workflows. For integrations with Cloudflare services, use bindings (D1, KV, R2, Queues). For integrations with AWS services still in use during migration, use fetch() with appropriate authentication.
3. Test with production-representative inputs. Workflow behaviour under retry conditions matters more than the happy path. Test step failures, timeout handling, and the behaviour of waitForEvent() with delayed and missing events.
4. Run shadow executions. Supply representative inputs to the replacement while suppressing or substituting production side effects. Compare decisions and outputs, and test retries deliberately. Shadowing a payment, email or provisioning step without an isolated receiver would perform the operation twice.
5. Switch new executions to Workflows. After shadow execution validates correctness, route new executions to Workflows. Existing Step Functions executions continue running to completion on the old system; running executions cannot be migrated mid-flight, and this is a property of durable execution systems rather than a limitation to engineer around. This coexistence period can last days or weeks depending on the duration of your longest-running workflows. If your longest Step Functions execution typically runs for 48 hours, plan for at least 48 hours of coexistence after routing 100% of new executions to Workflows. In practice, add a generous buffer: a workflow that typically runs 48 hours might occasionally run for a week due to retries or external delays.
6. Decommission Step Functions. Reconcile legacy executions, including failed or uncertain external effects, before disabling their entry points. Retain definitions, dependencies and execution evidence for the agreed repair and audit period; delete only after their recovery obligations are closed.
Let old executions finish under their original orchestrator while directing new business operations to Workflows. Executions may still share accounts, records and downstream services: preserve operation identities, permissions and compatible schemas across both. One valid orchestrator per execution does not establish one effect per business operation.
Playbook: AI inference services to Workers AI, Vectorize, and AI Gateway
Inference migration changes model behaviour as well as infrastructure. Compare the available hosted models and provider routes against the application's quality, latency, cost and data-handling requirements. Chapters 16–19 develop those decisions; do not infer equivalence from a shared API shape.
When Workers AI fits
Workers AI fits when a supported model meets the task requirements and managed inference removes infrastructure the team would otherwise operate. Measure the whole request path, including queueing, retrieval and tool calls. Calling a model through a binding does not establish lower latency than a particular external endpoint.
The platform supports several inference modalities, including text, embeddings, images and speech. Match the required model and modality to the current catalogue, then evaluate representative tasks. Hosting removes model-serving infrastructure work, but application versioning, evaluation and recovery remain.
When it doesn't
Retain an external provider when its model, custom deployment or operational contract is required. Workers AI supports selected hosted models and adapters rather than arbitrary model deployment. The cost of changing model behaviour can exceed the infrastructure saving; provider routing may meet the goal without replacing the model.
This is where AI Gateway becomes valuable. AI Gateway provides a unified proxy that routes requests to any supported provider (Workers AI, OpenAI, Anthropic, Azure OpenAI, Bedrock, Google AI Studio, and others) through a single endpoint. Rather than replacing your inference provider, AI Gateway gives you logging, caching, rate limiting, and fallback routing across all providers. Migration to AI Gateway provides operational benefits without requiring model changes.
Vector database migration
Vectorize replaces managed vector databases (Pinecone, Weaviate hosted, OpenSearch with knn plugin, pgvector) for applications within its capability envelope. Vectorize indexes support up to 20 million vectors with up to 1,536 dimensions, which covers most embedding models in common use.
If the embedding model, version, dimensions and preprocessing remain the same, existing vectors can be copied into a compatible Vectorize index. Preserve identifiers and metadata, and compare retrieval behaviour, filtering and relevance.
Changing the embedding model requires rebuilding the corpus embeddings and keeping query embeddings paired with the matching index. Evaluate that model change separately from the storage migration so a relevance difference has an identifiable cause.
Migration process
1. Deploy AI Gateway first. Route all existing inference requests through AI Gateway, pointing at your current provider. This adds logging, cost tracking, and caching without changing providers. The immediate benefit is visibility: you now know exactly how many inference requests you serve, at what latency, and at what cost.
2. Evaluate model quality. Run the same prompts through Workers AI models and your current provider. Compare output quality for your specific use case. Automated evaluation (BLEU scores, embedding similarity, classification accuracy) provides quantitative comparison; human evaluation provides qualitative assessment. If Workers AI models are insufficient, keep your current provider behind AI Gateway and skip steps 3 and 4.
3. Configure fallback routing. Set up AI Gateway to route requests to Workers AI with fallback to your current provider. If Workers AI returns an error or times out, the request automatically falls through to the fallback. This gives you the latency benefits of edge inference for successful requests while maintaining reliability through fallback.
4. Shift traffic gradually. Increase the proportion of requests routed to Workers AI as primary, monitoring quality metrics. AI Gateway's analytics show per-provider latency, error rates, and costs. If quality or latency degrades, adjust routing without code changes.
5. Migrate vector search independently. Copy compatible vectors, or rebuild embeddings if the model changes. Run comparison queries against both stores and verify metadata filters as well as relevance before switching the query path.
Keeping inference available during migration
Independent inference calls can move between providers gradually. Stateful conversations, tool calls and agent workflows need additional care: preserve their history and operation identifiers, and avoid repeating side effects when a request is retried elsewhere.
AI Gateway can provide a primary and fallback route, reducing exposure to one provider's failure. Both providers can fail, and timeouts, authentication errors or exhausted budgets may still reach the caller. Set an end-to-end timeout and define the user-visible failure or degraded result.
Shadow requests can compare model behaviour without changing the production response. Control the extra cost and data disclosure, and suppress tool side effects. If embedding models change, keep old queries on the old index until the new corpus is complete and evaluated, then switch each query to the matching model and index together.
Before decommissioning the old system
Account for every accepted operation, identify the authoritative writer and confirm how changes made after cutover would be recovered. Check queues, scheduled jobs, direct database clients and administrative tools as well as the main request path.
Compare the new system with the baseline, including operational work and failure recovery. Keep the old system only for a defined recovery purpose, with an owner and an exit condition. An idle copy that cannot recover current state adds cost without providing a usable rollback.
Getting help
Cloudflare provides migration support for enterprise customers; Solutions Architects help assess migration candidates, design sequences, troubleshoot issues, and validate outcomes.
For non-enterprise migrations, Cloudflare's documentation is comprehensive (though sometimes overwhelming), community forums vary in quality, and the Discord is active for specific questions. Edge cases require consulting specific documentation or community expertise.
Migration is investment, and the hours spent migrating don't ship features but recreate existing capability on new infrastructure. Invest wisely by quantifying expected benefits, migrating incrementally, maintaining rollback capability, and measuring outcomes. The goal isn't migration but rather the improvement migration enables.
What comes next
Chapter 28 closes with the questions worth carrying into the next design review: where state belongs, what must agree, how work recovers and which assumptions deserve an experiment before a commitment.