Skip to main content

Salesforce Closed Both Deploy Doors: The Complete Exit Map for Functions and Heroku Teams

12 min readDora NodaDora Noda
Share
On this page

On January 31, 2025, Salesforce Functions reached end of life. Thirteen months later, on February 6, 2026, Heroku's chief product officer announced the platform was moving to a sustaining engineering model: no new features, no new Enterprise Account contracts, ever again. Two different products, two different years, one combined message — every deploy target Salesforce ever offered you is now exit-only at the same time.

If your team holds both — Functions code wired into Salesforce events and Heroku dynos running the web/worker tier — you do not have two migrations. You have one migration with two fronts, and the order you tackle them in decides whether you migrate once or twice. This post is the single map: every workload type in one table, the data move, the compute move, and the sequence that avoids doing any of it over again.

The exit map: every workload type, one row each

Here is the whole migration on one screen. Each row is a workload type you hold today, where it actually goes, and the one-line reason. The sections below unpack the rows that bite.

You hold todayWhere it goesWhy
Salesforce Functions (event-driven code)Queue-consumer workers on an owned fleet, or serverless (Lambda / Cloud Run / Azure Functions) called via Named CredentialsFunctions was elastic compute with a Salesforce event envelope; the envelope is gone, so you bring your own queue and keep the Salesforce call going the other direction
Heroku web dynosGit-push deploys on Render, Railway, Fly.io, or a self-hosted PaaSThe dyno contract (buildpacks, config vars, release phase) ports almost line for line; pick the destination by region and cost needs, not by rewrite size
Heroku worker dynosWorkers on the same new platform, backed by your own queueWorkers were always "dyno plus Redis"; keep the pairing, move both halves together
Heroku PostgresBYO managed Postgres (Neon, Supabase, Crunchy, RDS) or a self-hosted operatorPostgres is Postgres — pg_dump out, restore in; the only real decision is who pages you at 3am
Heroku Redis / Key-ValueManaged KV (Upstash and friends) or self-hosted Redis/ValkeyCache and queue state rehydrates; this is the lowest-risk row on the table
Heroku Connect (Salesforce sync)Purpose-built sync/ETL tooling or direct API integrationSync was never compute; replace the pipe, not the app
Add-ons (logging, monitoring, scheduler)Standalone services wired by env varEvery add-on was already a third party behind a config var; reattach directly
Pipelines and review appsPreview environments on the new platformEvery serious alternative ships preview deploys; this row is config, not code

Two things to notice. First, nothing on this table requires a rewrite — the migration is a re-platforming, not a re-architecture.

Second, the rows have an order, and getting it wrong is the only way this migration gets expensive. Data first, then compute, then cutover. That ordering is the rest of this post.

What "sustaining engineering" concretely means

Before the mechanics, the premise, because it sets the urgency. Heroku's February 2026 announcement, signed by CPO Nitin T Bhat, says the platform is "transitioning to a sustaining engineering model focused on stability, security, reliability, and support" — actively supported and production-ready, but with "an emphasis on maintaining quality and operational excellence rather than introducing new features." Enterprise Account contracts are no longer offered to new customers. Existing customers see no pricing or service change.

Salesforce gave Functions the harder edge a year earlier: retired January 31, 2025, with customers told to "deploy an alternative solution before your existing Order Term ends," full stop. No sustaining mode, no long tail — the Heroku Dev Center hosts the retirement timeline and migration pointers, and the product is gone.

Analysts read the Heroku move as phase-out prep — "Heroku isn't exactly dead, but it may as well be," as one put it — and independent analysts openly advise planning the exit now while Salesforce is giving customers a window to act. The window is the point: nothing is on fire today, which is exactly when migrations are cheap. Forced migrations under a deadline are the ones that get done twice.

Move the data first: Postgres and Redis

Data has gravity; compute does not. A web dyno is re-deployable in minutes, but a database carries every row your business owns, and every shortcut taken on the data move becomes a second migration later. So the data rows move first, while the old compute still runs against them.

For Heroku Postgres, the mechanics are gloriously boring, because Postgres is Postgres. Small databases move with the standard dump-and-restore pair: pg_dump from the Heroku connection string, pg_restore into the new database.

Larger or uptime-sensitive databases move with logical replication — every major managed Postgres vendor publishes a Heroku-specific guide (Neon, Supabase, Crunchy, Render, and RDS-flavored paths all document the same shape), and the pattern is always seed-then-replicate-then-cutover. Test the restore on a staging copy before you touch production; a backup you have never restored is a rumor, not a backup.

The destination decision is really an ops decision: who do you want holding the pager? Managed Postgres (Neon, Supabase, Crunchy Data, RDS, or your new platform's bundled Postgres) keeps the Heroku bargain — backups, failover, and patching handled — at prices that undercut Heroku's data tier substantially. A Standard-0 Heroku Postgres at $50/month is the line item teams benchmark against, and most alternatives land well under it for equivalent storage and connection counts. Self-hosting via an operator (CloudNativePG and its peers) is the cheapest steady state and the most pager duty; it earns its place when you already run a fleet team, not as a side quest during a migration.

Redis is the easy row. Cache contents rehydrate and queue contents drain, so the move is: stand up the replacement (managed KV or self-hosted Redis/Valkey), point staging at it, then production during a quiet window. The one gotcha is session stores — if user sessions live in Redis, plan a session-drain or forced re-login rather than discovering it from the support queue.

Why data leads: the classic double migration is pointing shiny new compute at the old Heroku database "temporarily," then migrating the database six months later under time pressure and re-validating every query path. Move the data while the old compute still works, run the new database behind the old app for a proving period, and the compute move becomes a pure deploy with a known-good data layer underneath.

Then the compute: Functions code and dynos

With data settled, the two compute fronts move in parallel.

Salesforce Functions code becomes queue-consumer workers. A Function was elastic compute plus a Salesforce event envelope — platform events and record triggers in, results back into the org. The compute half ports directly: the same Node.js or Java logic becomes a worker process consuming from a queue (SQS, NATS, Redis Streams, or your fleet's default) on an owned fleet or a serverless target. What changes is the envelope. Salesforce can no longer invoke your code the Functions way, so the integration reverses direction: Salesforce calls out to your workers over HTTP via Named Credentials, or drops events onto a queue your workers already drain. Integration-heavy Functions — the ones that were really ETL with extra steps — belong on MuleSoft or a proper pipeline tool rather than being faithfully re-implemented as workers; the retirement is a good excuse to admit what the code actually was. Keep one discipline from the Functions era: the code was stateless and transactional, and it should stay that way. The moment a worker grows local state, you have re-invented the dyno filesystem problem below.

Heroku dynos become git-push deploys, nearly line for line. The dyno contract maps cleanly: buildpacks to buildpacks (or a Dockerfile, which every alternative also accepts), config vars to environment variables, Procfiles to the same process model, release-phase tasks to deploy hooks. Web dynos land on whatever web-service primitive the destination offers; worker dynos land on its worker primitive with the queue you already moved. Pricing is where the move pays for itself visibly: a Standard-1X dyno ($25/month for 0.5 GB) or Performance-M ($250/month for 2.5 GB) against, say, Render's 2 GB web service at $25/month tells the story — teams commonly report cutting the compute bill in half or better, with the gap widening as dyno counts grow.

The gotchas that bite every Heroku migration, in rough order of support tickets generated:

  • The 30-second router timeout. Heroku's router kills requests past 30 seconds, so years of code was shaped around it — chunked responses, backgrounded work, polling. Your new platform has different limits (or none at the same layer). Audit every endpoint that was designed around the timeout before you discover which ones secretly depended on it.
  • Ephemeral filesystems. Dynos famously discard local disk on restart, which forced good behavior (object storage for uploads) — unless someone found the one path that worked by accident. Grep for local writes before the move.
  • Add-on env var wiring. Each add-on injected its own config vars, and apps accrete references to them. Inventory every *_URL var and its consumer; each one is a reattachment task on the new platform, not a surprise at cutover.
  • Release phase and scheduler. release tasks and Heroku Scheduler jobs are easy to forget because they run outside request traffic. Enumerate them explicitly — the job nobody remembers is the one that pages you a week after cutover.
  • Review apps and pipelines. Recreate the promotion flow (staging auto-deploy, preview per PR) on the destination before the team needs it mid-migration, or developers will route around the new platform.

Destination choice, briefly: Render is the closest to old Heroku ergonomics and the fastest lift-and-shift; Railway wins on developer experience for small teams; Fly.io wins when you need multi-region; self-hosted options (Dokku, Coolify, or a Cluster-API fleet) win on cost and control once you have the ops appetite. The honest version is that all of them run the same containers — choose by region, price, and who holds the pager, not by framework support matrices.

The sequence that avoids migrating twice

Ordered, with what each step unlocks and what rework it prevents:

  1. Inventory everything. Every Function with its trigger, every dyno with its type and add-ons, every *_URL config var, every scheduled job, every review-app dependency. Output: a checklist, not a wiki page. Skipping this is how step 5 discovers a forgotten Scheduler job.
  2. Move the data. Postgres via dump-restore or logical replication, Redis via re-point. Run the new data layer behind the old compute for a proving period. Unlocks: every later step becomes a pure compute deploy. Prevents: the temporary-old-database that becomes permanent.
  3. Move the workers. Functions code to queue-consumers, worker dynos to the new worker primitive — against the already-moved data. Run old and new workers with the queue drained by exactly one side at a time. Unlocks: background processing validated before user traffic moves. Prevents: debugging data and compute in the same incident.
  4. Move the web tier. Deploy, soak on staging, then shift traffic (weighted DNS or a load-balancer split) with instant rollback to the old dynos. Unlocks: rollback that is a DNS change, not a re-deploy. Prevents: cutover as a point of no return.
  5. Cut over and decommission. DNS fully over, old workers drained and stopped, Heroku apps and Functions org connections deleted — after one full business cycle (month-end, billing run, whatever your scary periodic job is) on the new stack. Prevents: discovering the forgotten job after the old platform is gone.

The through-line: at every step, exactly one layer is new. New data with old compute, then new workers with proven data, then new web with proven everything. The teams that migrate twice are the teams that changed two layers at once and couldn't tell which one broke.

One honest boundary: nobody should run your database as a side effect

There is a reason the data rows in the map above say "BYO" and this section exists. A deploy platform that also runs your database has fused two failure domains with two different scaling laws and two different backup stories into one bill — convenient until the day you need to leave, when you discover the database was the lock-in and the compute was the demo. Heroku's integrated stack was exactly this bargain, and unwinding it is half the work described above.

Bex draws the line explicitly: it is a deploy platform, not a database vendor. Compute, routing, TLS, preview environments — yes. Your Postgres and your KV store live where you choose (managed vendor or your own operator), connected by a connection string, portable on day one because the platform never owned them.

That boundary is what makes the map above a re-platforming instead of a re-architecture: when state was never fused to compute, moving compute is just deploying.

Salesforce closing both deploy doors within thirteen months looks like bad luck. Read differently, it is a forcing function with a generous window: the teams that treat 2026 as the year they put compute and state on separately portable foundations will never have to migrate both at once again.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. If your Heroku exit plan starts with "git push somewhere else," star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex