Skip to main content
Redis by Upstash is deprecated and scheduled to be sunset. Its replacement is Laravel Valkey, a fully managed, Redis-compatible KV store with better performance and per-second billing. This guide walks through moving an environment from an Upstash cache to a Valkey cache.
Redis by Upstash pricing changes on November 30, 2026. Eligible customers who migrate to Laravel Valkey and delete their Upstash cache before that date will receive a Cloud credit based on two months of the cache’s monthly price. See the pricing page for the current and upcoming prices.Existing Redis by Upstash caches will remain available until August 31, 2027, when the service is sunset on Laravel Cloud.
Valkey is compatible with the Redis API your application already uses, so your application code does not need to change. Migrating is really about two things: pointing your environment at the new store, and deciding what, if anything, must happen to the data in the old one. Laravel Valkey is the right destination for most applications. Enterprise customers on Private Cloud may instead migrate to a managed AWS ElastiCache cache, available in both Valkey and Redis OSS engines, which adds automatic failover and multi-AZ deployments. Either way the destination is a Redis-compatible store, so every path in this guide applies unchanged. To provision ElastiCache, contact us.

Before you start

An environment has one attached cache at a time. Replacing it updates the injected REDIS_* environment variables on the next deployment. Detaching a KV store does not delete it, so your Upstash cache remains available and billable until you delete it from your organization’s Resources page. Its credentials remain valid, allowing a side-by-side migration. The UPSTASH_* and VALKEY_* variables used in this guide are temporary and used only by the migration tooling. Remove them when the migration is complete. First, work out what your application actually keeps in its KV store, since that determines how careful you need to be: Then pick your path:

Start fresh

If Upstash only backs your application’s cache, there is no data worth moving. Caches rebuild themselves.
1

Attach a Valkey cache

On your environment’s canvas, open the ”…” menu on the cache card and choose “Replace”, then select a Laravel Valkey cache, creating one if needed. Replacing only re-points the environment at the new cache; nothing changes for your running application until the next deployment.
2

Deploy

Redeploy the environment. The injected REDIS_* variables now point at Valkey.
3

Clean up

Once you have verified the application, delete the Upstash cache from your organization’s Resources page. Detached KV stores continue to bill until deleted.
The new cache starts empty, so cache reads miss until it rebuilds. On high-traffic applications, deploy during a low-traffic window to limit the temporary increase in database load. Redis-backed sessions reset, so users must sign in again and forms submitted across the cutover may receive a 419 Page Expired response. Use the session drain if that is unacceptable. Atomic locks and rate limiters also reset, so a withoutOverlapping scheduled task or a ShouldBeUnique job could run twice during the deployment.
If QUEUE_CONNECTION=redis, any jobs still in the old store, including delayed jobs, will never be processed after the swap. Check before you cut over by running php artisan queue:monitor redis:default from the Commands tab (list each queue name you dispatch to). If the count is not zero and those jobs matter, use Path 2 instead.

Drain your queues

Queue data changes while your application runs, so do not copy it between stores. Instead, run both stores side by side. New jobs dispatch to Valkey while a worker drains the jobs that remain on Upstash.
1

Save the Upstash credentials

Before swapping the cache, copy the current values of REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, and REDIS_SCHEME from your environment’s variables into new custom variables: UPSTASH_REDIS_HOST, UPSTASH_REDIS_PORT, UPSTASH_REDIS_PASSWORD, and UPSTASH_REDIS_SCHEME. Because these mirror the values your application connects with today, they are guaranteed to be in the right format.
2

Define the drain connections

Add a Redis connection for the old store in config/database.php:
And a queue connection that uses it in config/queue.php:
Commit these changes.
3

Swap the cache and deploy

Attach the Valkey cache to the environment and deploy. From this moment, every dispatch() lands on Valkey, since the default Redis connection now resolves to the new store. Your cache and sessions cut over in the same deployment, so the expectations from Path 1 apply to them here.
4

Run a drain worker

Add a background process running php artisan queue:work upstash and deploy. If you dispatch to multiple queues, list them: php artisan queue:work upstash --queue=default,emails. For a small backlog with no delayed jobs, a one-off run of php artisan queue:work upstash --stop-when-empty from the Commands tab is enough.
5

Watch until empty

Check progress from the Commands tab with php artisan queue:monitor upstash:default (again, listing each queue name). The reported size includes delayed and reserved jobs, so zero means genuinely done.Be careful relying on --stop-when-empty as your completion signal: it exits as soon as no job is ready right now, even if delayed jobs are still scheduled for the future. queue:monitor is the source of truth.
6

Tear down

Retry or delete failed jobs before removing the old connection, since a retried job is re-dispatched onto the connection recorded on the failed job. Then remove the drain worker, the two config blocks, and the UPSTASH_* variables, deploy, and delete the Upstash cache from the Resources page.
If you run Horizon, keep it pointed at the default connection and drain the old store with a plain queue:work background process. Horizon’s internal state lives in Redis, so its dashboard metrics reset at the swap. For job-level visibility that survives the migration, Laravel Nightwatch traces queue jobs independently of which store backs them.
If you are rethinking your queue setup anyway, consider draining to managed queues instead of Valkey. The pattern is identical: create the managed queue so new jobs dispatch to the cloud connection, then drain the old Redis store with the same worker and monitor commands.

Keeping users signed in

By default, sessions reset at the swap and users sign in again. If that is unacceptable (long-lived logins, carts kept in the session, an active admin panel), sessions can be drained just like queues, using a temporary read-through session handler that checks the new store first and falls back to the old one. Laravel saves the session at the end of every request. After cutover, the first request from a returning visitor reads their session from Upstash through the fallback and writes it to Valkey. Redis-backed sessions expire SESSION_LIFETIME minutes (120 by default) after the user’s last request, so keep the fallback in place for one session lifetime after cutover. Any session that remains on Upstash after that period has expired normally. Reusing the upstash Redis connection from the drain setup, define a cache store for it in config/cache.php:
Create the handler:
Register it as a session driver in the boot method of app/Providers/AppServiceProvider.php:
Set SESSION_DRIVER=drain so it deploys together with the cache swap. Note that destroy clears both stores, so signing out cannot be undone by the fallback, and rolling back mid-window is just an environment variable change. On Laravel Cloud, the injected configuration points the default and cache Redis connections at the same store and database, so the keys written here line up with the built-in redis session driver. Once one session lifetime has passed after cutover, set SESSION_DRIVER back to redis and re-deploy before deleting the Upstash cache. With the driver reverted, remove the handler, cache store, and connection with the rest of the drain teardown. Applications with very long session lifetimes should note the old cache must stay alive (and billing) for the whole window.

Copy the data

Sometimes the data itself must survive: your application stores long-lived values via the Redis facade, sessions you cannot invalidate, cache entries too expensive to rebuild, or delayed jobs scheduled further out than you can reasonably drain. Upstash does not expose Redis replication commands (REPLICAOF, PSYNC) to clients, so a live, zero-downtime replication into Valkey is not possible. Restoring an Upstash backup is not possible either: Upstash runs Redis 8.x, whose export format Valkey, which is Redis 7.2-compatible, cannot load, and DUMP/RESTORE fails for the same reason. The reliable path is a client-side copy that reads each key with its type’s native commands and writes it to the new store, which is exactly what the Artisan command below does. A copy like this is point-in-time. Anything written to the old store after its key has been copied is silently lost, so you must freeze writes for the duration. This path requires a maintenance window roughly the length of the copy plus a deployment.
1

Prepare (no downtime yet)

Create the Valkey cache but do not attach it. Click the ”…” icon next to the cache on the Resources page, then “View credentials”, and add the connection details as custom environment variables: VALKEY_REDIS_HOST, VALKEY_REDIS_PORT, VALKEY_REDIS_USERNAME, and VALKEY_REDIS_PASSWORD. Copy the current injected REDIS_* values into UPSTASH_* variables as in Path 2.Set the maintenance mode variables so the “down” state is shared across instances and survives the cache swap:
The store must be database, not redis. Maintenance state kept in the cache you are migrating away from would be lost mid-migration.Add both Redis connections to config/database.php:
The empty prefix matters. SCAN returns raw keys that already carry your application’s key prefix, so the copy connections must not apply a prefix of their own or every key would be double-prefixed on write. With an empty prefix, keys are copied byte-for-byte.Finally, add the cache:migrate command to your application, commit, and deploy. Nothing has changed for your users yet.
2

Count the keyspace

From the Commands tab, run php artisan cache:migrate --dry-run to count keys by type without copying anything. As a rough planning figure, expect the copy to move a few hundred keys per second, and size your maintenance window accordingly.
3

Freeze writes

Run php artisan down from the Commands tab, then stop everything else that writes to Redis: pause any managed queues, remove or scale down queue:work background processes, and disable the Scheduler toggle on your compute cluster (these changes deploy with the environment). php artisan down only stops web traffic; workers and the scheduler keep running until you stop them explicitly.
4

Copy

Run php artisan cache:migrate from the Commands tab. The command replaces each destination key before writing, so it is idempotent and safe to re-run if interrupted.
5

Swap and verify

Attach the Valkey cache to the environment and deploy. Then compare key counts by running php artisan cache:migrate --from=valkey --dry-run against the source’s earlier count. A small difference is normal, since keys with short TTLs expire during the window.
6

Resume

Run php artisan up, re-enable the scheduler and workers, and resume any paused queues. After a burn-in period, remove the migration command, config blocks, and UPSTASH_* / VALKEY_* variables, and delete the Upstash cache from the Resources page.

The migration command

Use this command to copy every key from one Redis connection to another using each type’s native read and write commands, preserving TTLs. It is written for phpredis, Laravel’s default Redis client. If your application sets REDIS_CLIENT=predis, a few calls (such as zAdd argument order) need adapting.
The command skips Redis streams, since consumer group state cannot be copied faithfully this way, and it reads each key’s value in a single call, so extremely large individual keys (a hash with millions of fields, for example) may need chunked copying with HSCAN instead. Most Laravel applications are unaffected by either limitation.

Using an AI agent

An AI coding agent can help with this migration when it has this guide, your application’s repository, and an authenticated Laravel Cloud CLI. Ask the agent to assess the application first, then approve each user-facing or irreversible step yourself.

Need help?

If you get stuck at any point, or you have an unusual cache workload you are not sure how to migrate, contact support from the Help portal in your Laravel Cloud dashboard before cutting over. It is far easier to talk through the approach in advance than to recover a migration that has already gone sideways. When you reach out, mention which path you are on and what your KV store is used for (cache, sessions, queues, or direct Redis usage) so the team can help quickly.