{"id":14882,"date":"2026-10-03T17:30:00","date_gmt":"2026-10-03T12:00:00","guid":{"rendered":"https:\/\/www.allerin.com\/blog\/?p=14882"},"modified":"2026-09-25T11:20:57","modified_gmt":"2026-09-25T05:50:57","slug":"heroku-exit-data-config","status":"publish","type":"post","link":"https:\/\/www.allerin.com\/blog\/heroku-exit-data-config\/","title":{"rendered":"Heroku exit checks for data, jobs and signing keys"},"content":{"rendered":"<p>Before a Heroku exit, account for the application&#8217;s last accepted write, pending jobs and existing sessions on the destination. A successful deploy does not establish any of those things.<\/p>\n<p>Heroku&#8217;s <a href=\"https:\/\/www.heroku.com\/blog\/an-update-on-heroku\/\" target=\"_blank\" rel=\"noopener\">6 February 2026 announcement<\/a> said, \u201cToday, Heroku is transitioning to a sustaining engineering model focused on stability, security, reliability, and support.\u201d It did not announce a platform shutdown. Heroku Ruby engineer Richard Schneeman <a href=\"https:\/\/www.reddit.com\/r\/rails\/comments\/1qxng79\/comment\/o3zp4z6\/\" target=\"_blank\" rel=\"noopener\">also clarified that distinction<\/a> in a personal comment.<\/p>\n<p>There is a specific stack deadline. <a href=\"https:\/\/help.heroku.com\/NQNCQTEJ\/heroku-22-end-of-life-faq\" target=\"_blank\" rel=\"noopener\">Heroku-22 reaches end of life on 30 April 2027<\/a>; builds stop on 1 May. Existing applications are not automatically switched off. Facts below were checked on 7 September 2026.<\/p>\n<nav style=\"margin: 24px 0; padding: 18px; background: #edf4f0; border-radius: 6px;\" aria-label=\"Article sections\">\n<p style=\"margin: 0 0 12px;\"><strong>Find the relevant check<\/strong><\/p>\n<ul style=\"display: flex; flex-wrap: wrap; gap: 10px 24px; list-style: none; margin: 0; padding: 0;\">\n<li style=\"margin: 0;\"><a href=\"#staying-can-be-the-right-engineering-decision\">Staying can be the right engineering decision<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#prove-the-database-restore-before-estimating-the-window\">Prove the database restore before estimating the window<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#redis-and-scheduled-work-need-their-own-inventory\">Redis and scheduled work need their own inventory<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#move-configuration-according-to-its-job\">Move configuration according to its job<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#rehearse-the-last-write-and-the-first-new-write\">Rehearse the last write and the first new write<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#what-to-do-this-week\">What to do this week<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#reproduce-the-heroku-exit-checks\">Reproduce the Heroku exit checks<\/a><\/li>\n<\/ul>\n<\/nav>\n<h2 id=\"staying-can-be-the-right-engineering-decision\" style=\"scroll-margin-top: 128px;\">Staying can be the right engineering decision<\/h2>\n<p>An application with a supported runtime, suitable operational controls and a tested recovery procedure can reasonably stay. List the requirement that Heroku cannot meet, then compare that gap with the work of operating the destination. Include who responds to failed backups, expired certificates and a stalled deployment.<\/p>\n<p><a href=\"https:\/\/devcenter.heroku.com\/changelog-items\/3703\" target=\"_blank\" rel=\"noopener\">Heroku-26 became generally available on 20 May 2026<\/a>, while Heroku-24 remains the documented default. Assess the stack&#8217;s Ruby support and native packages against the application&#8217;s lockfile. A supported stack move can resolve an operating-system deadline without relocating the database or replacing Sidekiq.<\/p>\n<p>The <a href=\"https:\/\/devcenter.heroku.com\/changelog-items\/2502\" target=\"_blank\" rel=\"noopener\">end of free services on 28 November 2022<\/a> and the February announcement concern different decisions. Neither establishes that a particular application should leave today. If its restore fails, or nobody owns the destination&#8217;s on-call work, postponing the exit is a defensible outcome.<\/p>\n<h2 id=\"prove-the-database-restore-before-estimating-the-window\" style=\"scroll-margin-top: 128px;\">Prove the database restore before estimating the window<\/h2>\n<p><a href=\"https:\/\/devcenter.heroku.com\/articles\/heroku-postgres-backups\" target=\"_blank\" rel=\"noopener\">PGBackups guidance<\/a> covers moderately loaded databases up to 20 GB. That is operating guidance, not a hard archive-size limit. For larger or busy databases, Heroku recommends considering a <a href=\"https:\/\/help.heroku.com\/7U1BTYHB\/how-can-i-take-a-logical-backup-of-large-heroku-postgres-databases\" target=\"_blank\" rel=\"noopener\">direct logical dump from a short-lived fork<\/a>, rather than burdening the primary or a follower. PGBackups is not currently supported on Postgres Advanced.<\/p>\n<p>For an eligible Classic database, the documented capture\/download path is <code style=\"overflow-wrap: anywhere; white-space: normal;\">heroku pg:backups:capture<\/code> followed by <code style=\"overflow-wrap: anywhere; white-space: normal;\">heroku pg:backups:download<\/code>, with the source app and database selected explicitly. Those account operations were not run for this article. The executed fallback was a local PostgreSQL 15.17 source and a disposable destination on the same major version.<\/p>\n<p>The rehearsal used 5,000 synthetic records. In the companion&#8217;s configured local shell, these commands make a custom archive and restore it with two workers. The destination database is disposable; <code style=\"overflow-wrap: anywhere; white-space: normal;\">--clean<\/code> removes objects that the archive will recreate. Never point this example at a live destination.<\/p>\n<pre style=\"overflow-x: auto; max-width: 100%;\"><code class=\"language-sh\">set -eu\r\npg_dump --version\r\npg_restore --version\r\npg_dump --format=custom --verbose \\\r\n  --dbname=ci69_source --file=tmp\/source.dump\r\npg_restore --clean --if-exists --no-owner --no-acl \\\r\n  --exit-on-error --jobs=2 --verbose \\\r\n  --dbname=ci69_restored tmp\/source.dump\r\n<\/code><\/pre>\n<p>Check both server versions as well. PostgreSQL documents which <a href=\"https:\/\/www.postgresql.org\/docs\/15\/app-pgdump.html\" target=\"_blank\" rel=\"noopener\">source versions a dump client can read<\/a>; restoring into an older major is not a supported downgrade method. <code style=\"overflow-wrap: anywhere; white-space: normal;\">--no-owner --no-acl<\/code> omits original ownership and grants, so establish and test destination roles separately.<\/p>\n<p>Run the same inspection against source and restore. These queries cover rows, the next identity value, indexes and extension placement. They are the small fixture&#8217;s checks, not an automatic inventory of an arbitrary application.<\/p>\n<pre style=\"overflow-x: auto; max-width: 100%;\"><code class=\"language-sql\">SELECT count(*) FROM records;\r\nSELECT last_value, is_called FROM records_id_seq;\r\nSELECT max(id) FROM records;\r\nSELECT indexrelid::regclass, indisvalid, indisready\r\nFROM pg_index WHERE indrelid = 'records'::regclass;\r\nSELECT extname, extversion, extnamespace::regnamespace\r\nFROM pg_extension ORDER BY extname;\r\nSELECT datcollate, datctype, datcollversion,\r\n       pg_database_collation_actual_version(oid)\r\nFROM pg_database WHERE datname = current_database();\r\n<\/code><\/pre>\n<p>The single local run measured 0.051 seconds for the dump and 0.041 seconds for restoration. The source occupied 10,214,759 bytes. These timings describe this small fixture, not a production outage estimate.<\/p>\n<p>The download also compares ordered content fingerprints. They caught a changed value even though the row count stayed at 5,000. A deliberately stale sequence failed a separate check. Run <code style=\"overflow-wrap: anywhere; white-space: normal;\">ANALYZE<\/code> after restoration, then use read-only application checks and verify actual destination permissions.<\/p>\n<p>Extension availability includes version and namespace, not just the gem in the lockfile. Inventory PostGIS, vector, <code style=\"overflow-wrap: anywhere; white-space: normal;\">pg_stat_statements<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">pgcrypto<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">uuid-ossp<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">citext<\/code> and <code style=\"overflow-wrap: anywhere; white-space: normal;\">hstore<\/code> where used. Heroku required <code style=\"overflow-wrap: anywhere; white-space: normal;\">heroku_ext<\/code> <a href=\"https:\/\/devcenter.heroku.com\/changelog-items\/2446\" target=\"_blank\" rel=\"noopener\">from August 2022<\/a>, then <a href=\"https:\/\/devcenter.heroku.com\/changelog-items\/2662\" target=\"_blank\" rel=\"noopener\">removed the requirement for existing databases in August 2023<\/a>. The current <a href=\"https:\/\/devcenter.heroku.com\/articles\/heroku-postgres-extensions\" target=\"_blank\" rel=\"noopener\">extensions page<\/a> uses <code style=\"overflow-wrap: anywhere; white-space: normal;\">public<\/code> by default. Existing extensions can retain their original schemas. The fixture deliberately placed <code style=\"overflow-wrap: anywhere; white-space: normal;\">citext<\/code> in <code style=\"overflow-wrap: anywhere; white-space: normal;\">heroku_ext<\/code>. After restoration, the unchanged Rails lookup for <code style=\"overflow-wrap: anywhere; white-space: normal;\">ref-000001<\/code> failed against the stored <code style=\"overflow-wrap: anywhere; white-space: normal;\">REF-000001<\/code>. Adding <code style=\"overflow-wrap: anywhere; white-space: normal;\">public, heroku_ext<\/code> to the application connection&#8217;s search path made it pass. PostgreSQL documents that <a href=\"https:\/\/www.postgresql.org\/docs\/15\/citext.html\" target=\"_blank\" rel=\"noopener\">missing citext operators cause case-sensitive comparison<\/a>. Only add <a href=\"https:\/\/www.postgresql.org\/docs\/15\/ddl-schemas.html#DDL-SCHEMAS-PATH\" target=\"_blank\" rel=\"noopener\">trusted schemas with controlled CREATE privileges<\/a>. Correct rows and types had not established correct application behavior.<\/p>\n<p>A fresh logical restore builds new indexes under the destination&#8217;s collation rules. Different rules can change ordering or uniqueness and can make restoration fail. That differs from retaining old index files after an operating-system collation update. For a recorded\/actual version mismatch, <a href=\"https:\/\/www.postgresql.org\/docs\/15\/sql-altercollation.html#SQL-ALTERCOLLATION-NOTES\" target=\"_blank\" rel=\"noopener\">PostgreSQL requires affected objects to be rebuilt before refreshing the recorded version<\/a>. The <code style=\"overflow-wrap: anywhere; white-space: normal;\">C<\/code>-locale fixture does not reproduce a glibc or ICU change.<\/p>\n<p>Do not promise a universal replication shortcut. Heroku&#8217;s <a href=\"https:\/\/devcenter.heroku.com\/articles\/heroku-postgres-advanced\" target=\"_blank\" rel=\"noopener\">current Postgres comparison<\/a> lists managed logical replication as unsupported for Classic and planned for Advanced. A separate <a href=\"https:\/\/devcenter.heroku.com\/articles\/heroku-data-connectors\" target=\"_blank\" rel=\"noopener\">Kafka streaming connector<\/a> provides an outbound path for eligible databases. Neither proves that this application&#8217;s destination supports a complete, lossless cutover.<\/p>\n<h2 id=\"redis-and-scheduled-work-need-their-own-inventory\" style=\"scroll-margin-top: 128px;\">Redis and scheduled work need their own inventory<\/h2>\n<p>Stopping web requests does not stop webhook consumers, schedulers or other writers. Stop producers first, finish in-flight work, and only then stop workers. <a href=\"https:\/\/devcenter.heroku.com\/articles\/maintenance-mode\" target=\"_blank\" rel=\"noopener\">Heroku maintenance mode<\/a> controls incoming router traffic on Cedar; it is unavailable on Fir. It is not a database write lock and does not stop Scheduler.<\/p>\n<div style=\"overflow-x: auto;\" tabindex=\"0\" role=\"region\" aria-label=\"Application state and the decisions needed before cutover\">\n<table style=\"width: 100%; min-width: 620px; table-layout: auto; border-collapse: collapse; font-size: 16px; line-height: 1.55;\" aria-label=\"Application state and the decisions needed before cutover\">\n<thead>\n<tr>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">State<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Decision before cutover<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Sidekiq queues, retries and scheduled jobs<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Preserve payloads and their execution times, or record how each will complete. A zero queue length ignores retries, scheduled work and busy workers.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Redis cache, sessions and counters<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">A rebuildable cache can warm again. Losing sessions or rate-limit counters changes behavior and needs an explicit decision.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Action Cable pub\/sub<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Plan reconnects and missed notifications. Pub\/sub is not a durable job archive.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Active Storage<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Transfer or retain both blobs and database references. Test download and upload permissions separately.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Scheduler and add-ons<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Record command, cadence, timezone, owning app, shared attachments and provider export\/retention terms. Give each recurring task one active scheduler.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>The companion&#8217;s <a href=\"https:\/\/github.com\/sidekiq\/sidekiq\/wiki\/API\" target=\"_blank\" rel=\"noopener\">Sidekiq API<\/a> probe reads queues, retries, scheduled jobs and <code style=\"overflow-wrap: anywhere; white-space: normal;\">WorkSet<\/code> busy counts. It was tested with Redis writes denied. It avoids <code style=\"overflow-wrap: anywhere; white-space: normal;\">ProcessSet<\/code>&#8216;s default cleanup behavior. Heartbeat information can lag, and the reads are not atomic; even zero counts, including the dead set, cannot prove that every producer has stopped.<\/p>\n<p>Run the check from the companion directory with its connection configuration. It observed one busy, queued, retry and scheduled job. After the synthetic entries were cleared and the active job finished, a deliberately late producer made the check fail again. Clearing fixture entries is not a production drain procedure.<\/p>\n<pre style=\"overflow-x: auto; max-width: 100%;\"><code class=\"language-ruby\">require \"json\"\r\nrequire_relative \"queue_connection\"\r\n\r\nqueues = Sidekiq::Queue.all.to_h { |queue| [ queue.name, queue.size ] }\r\ncounts = {\r\n  busy: Sidekiq::WorkSet.new.size,\r\n  queued: queues.values.sum,\r\n  retry: Sidekiq::RetrySet.new.size,\r\n  scheduled: Sidekiq::ScheduledSet.new.size,\r\n  dead: Sidekiq::DeadSet.new.size\r\n}\r\nputs JSON.pretty_generate(counts: counts, queues: queues,\r\n  observation: \"not atomic; busy follows worker heartbeats; intake stop is not verified\")\r\nexit(counts.values.all?(&amp;:zero?) ? 0 : 1)\r\n<\/code><\/pre>\n<p>The recurring summary command ran twice for the same reporting date and retained one summary row. That tests repeat handling; no persistent scheduler or Heroku Scheduler job was installed.<\/p>\n<p>Rails 8.0&#8217;s <a href=\"https:\/\/rubyonrails.org\/2024\/11\/7\/rails-8-no-paas-required\" target=\"_blank\" rel=\"noopener\">2024 release<\/a> introduced Solid components as defaults for new applications. Moving existing Sidekiq payloads to Solid Queue, or Redis semantics to Solid Cache\/Cable, is a separate migration. Keep those backends fixed during the hosting rehearsal.<\/p>\n<h2 id=\"move-configuration-according-to-its-job\" style=\"scroll-margin-top: 128px;\">Move configuration according to its job<\/h2>\n<p>A config export is an inventory containing secrets. Keep the actual export in restricted storage and out of logs, tickets and this public example. Classify names before assigning destination values.<\/p>\n<div style=\"overflow-x: auto;\" tabindex=\"0\" role=\"region\" aria-label=\"Configuration groups and their destination treatment\">\n<table style=\"width: 100%; min-width: 620px; table-layout: auto; border-collapse: collapse; font-size: 16px; line-height: 1.55;\" aria-label=\"Configuration groups and their destination treatment\">\n<thead>\n<tr>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Group<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Examples and treatment<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Application settings to review and carry<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Feature flags, locale and asset configuration. Verify the intended production value.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Credentials whose continuity matters<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Deliver <code style=\"overflow-wrap: anywhere; white-space: normal;\">RAILS_MASTER_KEY<\/code> securely and preserve the effective <code style=\"overflow-wrap: anywhere; white-space: normal;\">SECRET_KEY_BASE<\/code>. Plan any rotation separately.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Destination-specific connections<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Database\/Redis URLs, storage permissions, SMTP and callback URLs. Provision and test replacement credentials.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Platform and build settings<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><code style=\"overflow-wrap: anywhere; white-space: normal;\">PORT<\/code>, process counts, logging, static-file serving and memory tuning. Re-establish each behavior in the target runtime.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>Rails <a href=\"https:\/\/guides.rubyonrails.org\/5_2_release_notes.html#credentials\" target=\"_blank\" rel=\"noopener\">5.2 introduced encrypted credentials in 2018<\/a>; <a href=\"https:\/\/guides.rubyonrails.org\/6_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">6.0 added environment-specific credentials in 2019<\/a>. The master key decrypts that file. The effective signing secret helps derive keys for cookies and other signed\/encrypted data. Copying one while silently generating the other can invalidate existing sessions.<\/p>\n<p>A synthetic Rails CookieStore session survived with the same signing key and became unreadable after that key changed. Separately, copied encrypted content decrypted with its preserved key and rejected a changed key. These checks establish the two different continuity requirements; they do not test an existing application&#8217;s sessions.<\/p>\n<p>Rails <a href=\"https:\/\/guides.rubyonrails.org\/8_1_release_notes.html#command-line-credentials-fetching\" target=\"_blank\" rel=\"noopener\">8.1 added <code style=\"overflow-wrap: anywhere; white-space: normal;\">credentials:fetch<\/code><\/a> for retrieving a value from the encrypted store, including deployment secrets. This is a CLI improvement, not a new reason to rotate keys during a hosting move. It was not used in the fixed Rails 8.0 rehearsal.<\/p>\n<h3 id=\"recreate-deployment-and-proxy-behavior\" style=\"scroll-margin-top: 128px;\">Recreate deployment and proxy behavior<\/h3>\n<p>Heroku&#8217;s <a href=\"https:\/\/www.heroku.com\/blog\/celadon_cedar\/\" target=\"_blank\" rel=\"noopener\">2011 Cedar design<\/a> made the process model and declared processes central to deployment. Rails <a href=\"https:\/\/guides.rubyonrails.org\/7_1_release_notes.html#generate-dockerfiles-for-new-rails-applications\" target=\"_blank\" rel=\"noopener\">7.1&#8217;s generated Docker files in 2023<\/a> and 8.0&#8217;s Kamal 2\/Thruster defaults make a different deployment path available. They do not reproduce an existing Procfile, release phase or Preboot arrangement automatically.<\/p>\n<p>Compare classic buildpacks with <a href=\"https:\/\/devcenter.heroku.com\/articles\/ruby-app-behavior#ruby-cloud-native-buildpack-config-vars\" target=\"_blank\" rel=\"noopener\">Cloud Native Buildpacks<\/a> explicitly. The latter document <code style=\"overflow-wrap: anywhere; white-space: normal;\">RAILS_LOG_TO_STDOUT<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">RAILS_SERVE_STATIC_FILES<\/code> and <code style=\"overflow-wrap: anywhere; white-space: normal;\">MALLOC_ARENA_MAX=2<\/code>; do not assume identical injection across generations. Record the actual Ruby\/Bundler versions, native libraries, asset build, stdout logging, static-file handling and worker concurrency. <code style=\"overflow-wrap: anywhere; white-space: normal;\">PORT<\/code> is a platform contract. Carrying a historical <code style=\"overflow-wrap: anywhere; white-space: normal;\">MALLOC_ARENA_MAX<\/code> value is not a substitute for measuring memory in the destination image.<\/p>\n<p>Recreate the <a href=\"https:\/\/www.heroku.com\/blog\/announcing-release-phase-run-tasks-before-new-release-deployed\/\" target=\"_blank\" rel=\"noopener\">release phase, generally available since 2017<\/a>, and readiness behavior deliberately; do not run migrations concurrently in every web\/worker startup. Heroku documents a <a href=\"https:\/\/devcenter.heroku.com\/articles\/http-routing\" target=\"_blank\" rel=\"noopener\">30-second initial response limit and a rolling 55-second window<\/a>. Verify generation-specific routing behavior and the destination proxy&#8217;s streaming and timeout settings.<\/p>\n<p>At TLS termination, verify forwarded scheme, request IDs and the <a href=\"https:\/\/guides.rubyonrails.org\/configuring.html\" target=\"_blank\" rel=\"noopener\">Rails proxy settings<\/a>. Rails 7.1 added <code style=\"overflow-wrap: anywhere; white-space: normal;\">assume_ssl<\/code> for a verified TLS-terminating proxy; it does not install a certificate. Rails 6.0 added host authorization, but an empty <code style=\"overflow-wrap: anywhere; white-space: normal;\">config.hosts<\/code> list disables that check. Update the allowlist when enabled. Trusted proxies govern client-IP interpretation. Test redirects, secure cookies and generated URLs. Inventory DNS, certificate renewal, SSO callbacks and outbound IP allowlists before switching traffic.<\/p>\n<h2 id=\"rehearse-the-last-write-and-the-first-new-write\" style=\"scroll-margin-top: 128px;\">Rehearse the last write and the first new write<\/h2>\n<ol>\n<li><strong>Before the window<\/strong>, restore a recent copy and time capture, transfer, restore and verification. Exercise one recurring command and record the owner of each check. Confirm certificates and external callbacks.<\/li>\n<li><strong>At the write freeze<\/strong>, stop every producer and scheduler, drain accepted work, then stop workers. Confirm no remaining writer. Take the final capture and retain its identity.<\/li>\n<li><strong>Before destination writes<\/strong>, restore, inspect rows\/content\/sequences\/indexes\/extensions, refresh statistics and smoke-test reads. Keep the old application frozen and its add-ons intact.<\/li>\n<li><strong>At traffic switch<\/strong>, permit one destination writer and one scheduler. Watch errors, queue age, database connections and application-level reconciliation.<\/li>\n<li><strong>If rollback becomes necessary<\/strong>, determine whether the destination accepted writes. After that point, DNS reversal alone loses or divides data. Use a rehearsed reverse-sync\/reconciliation procedure, or explicitly accept the identified loss.<\/li>\n<\/ol>\n<p>The write-freeze window includes all these operations, not just <code style=\"overflow-wrap: anywhere; white-space: normal;\">pg_restore<\/code>. Local restore speed cannot estimate network transfer, a busy production database or external callback changes. Keeping the old app is useful only when its data state is understood. Review <a href=\"https:\/\/devcenter.heroku.com\/articles\/add-on-ownership-model-for-add-on-partners\" target=\"_blank\" rel=\"noopener\">add-on ownership<\/a> before deleting anything.<\/p>\n<h2 id=\"what-to-do-this-week\" style=\"scroll-margin-top: 128px;\">What to do this week<\/h2>\n<ul>\n<li>Record the concrete reason to leave, or the supported-stack work needed to stay.<\/li>\n<li>Restore a representative copy and prove that your checks reject a stale sequence and changed data.<\/li>\n<li>Inventory every Redis role, recurring task, secret and external callback with an accountable owner.<\/li>\n<li>Rehearse the write freeze and the rollback decision without changing Rails or queue backends in the same operation.<\/li>\n<\/ul>\n<h2 id=\"reproduce-the-heroku-exit-checks\" style=\"scroll-margin-top: 128px;\">Reproduce the Heroku exit checks<\/h2>\n<p><a href=\"https:\/\/www.allerin.com\/downloads\/rails\/heroku-exit-checks.zip\">Download the reproducible Heroku exit checks<\/a>. The AI-assisted example and review used Ruby 3.4.10, Rails 8.0.5.1, PostgreSQL 15.17, Sidekiq 7.3.10 and Redis 8.0.2 on macOS. All records and keys are synthetic. It tests local behavior, with no Heroku account, production data or remote cutover. The archive includes commands, SQL, configuration classification and expected failures. The fixed versions describe the experiment, not a production upgrade recommendation.<\/p>\n<p>For framework work, the <a href=\"https:\/\/www.allerin.com\/services\/rails-upgrades\">Rails upgrades guide<\/a> and <a href=\"https:\/\/www.allerin.com\/services\/ruby-on-rails\/support-calendar\">support calendar<\/a> describe separate decisions. The <a href=\"https:\/\/www.allerin.com\/services\/ruby-on-rails\/library\">Rails engineering library<\/a> collects the related investigations.<\/p>\n<p>Sources checked on 7 September 2026 include the <a href=\"https:\/\/www.heroku.com\/blog\/an-update-on-heroku\/\" target=\"_blank\" rel=\"noopener\">Heroku announcement<\/a>, <a href=\"https:\/\/help.heroku.com\/NQNCQTEJ\/heroku-22-end-of-life-faq\" target=\"_blank\" rel=\"noopener\">stack deadline<\/a>, <a href=\"https:\/\/devcenter.heroku.com\/articles\/heroku-postgres-backups\" target=\"_blank\" rel=\"noopener\">backup guidance<\/a>, <a href=\"https:\/\/www.postgresql.org\/docs\/15\/app-pgrestore.html\" target=\"_blank\" rel=\"noopener\">PostgreSQL restore reference<\/a>, <a href=\"https:\/\/github.com\/sidekiq\/sidekiq\/wiki\/API\" target=\"_blank\" rel=\"noopener\">Sidekiq API<\/a> and <a href=\"https:\/\/guides.rubyonrails.org\/configuring.html\" target=\"_blank\" rel=\"noopener\">Rails configuration guide<\/a>. Cutover ordering is engineering advice based on those facts and the application&#8217;s actual writers.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A measured local rehearsal checks restored data, queued work and signing secrets, with a cutover checklist and a reasoned case for staying on Heroku.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[2037],"tags":[2048,2064,2066,2065,2052],"class_list":["post-14882","post","type-post","status-publish","format-standard","hentry","category-ruby-on-rails","tag-heroku","tag-postgresql","tag-rails-credentials","tag-rails-deployment","tag-sidekiq"],"_links":{"self":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14882","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/comments?post=14882"}],"version-history":[{"count":2,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14882\/revisions"}],"predecessor-version":[{"id":14896,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14882\/revisions\/14896"}],"wp:attachment":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/media?parent=14882"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/categories?post=14882"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/tags?post=14882"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}