How to Gracefully Shut Down Redis
Method 1: The shutdown command
Redis provides the shutdown command to stop the service.
SHUTDOWN [NOSAVE | SAVE] [NOW] [FORCE] [ABORT]

ACL categories: @admin, @slow, @dangerous
Its behavior is as follows:
- If there are any lagging replicas:
- Run
CLIENT PAUSE WRITEon clients attempting to write, pausing their writes. - Wait up to the configured
shutdown-timeout(default 10 seconds) for replicas to catch up with the master’s replication offset.
- Run
- Stop all client connections.
- If at least one SAVE is configured, perform a blocking SAVE (write the data snapshot to the RDB file).
- If AOF (Append Only File) is enabled, flush the AOF buffer to disk.
- Exit the Redis server.
The log from running the shutdown command:
70684:M 20 Jan 2026 11:55:51.250 * Ready to accept connections
70684:M 20 Jan 2026 11:56:22.323 # User requested shutdown... # redis service is shut down
70684:M 20 Jan 2026 11:56:22.323 * Saving the final RDB snapshot before exiting.
70684:M 20 Jan 2026 11:56:22.325 * DB saved on disk
70684:M 20 Jan 2026 11:56:22.325 * Removing the pid file.
70684:M 20 Jan 2026 11:56:22.325 # Redis is now ready to exit, bye bye...
Let’s look at the source code of the shutdown command in Redis:
void shutdownCommand(client *c) {
int flags = 0;
if (c->argc > 2) {
addReply(c,shared.syntaxerr);
return;
} else if (c->argc == 2) {
if (!strcasecmp(c->argv[1]->ptr,"nosave")) {
flags |= SHUTDOWN_NOSAVE;
} else if (!strcasecmp(c->argv[1]->ptr,"save")) {
flags |= SHUTDOWN_SAVE;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
if (prepareForShutdown(flags) == C_OK) exit(0);
addReplyError(c,"Errors trying to SHUTDOWN. Check logs.");
}
int prepareForShutdown(int flags) {
/* When SHUTDOWN is called while the server is loading a dataset in
* memory we need to make sure no attempt is performed to save
* the dataset on shutdown (otherwise it could overwrite the current DB
* with half-read data).
*
* Also when in Sentinel mode clear the SAVE flag and force NOSAVE. */
if (server.loading || server.sentinel_mode)
flags = (flags & ~SHUTDOWN_SAVE) | SHUTDOWN_NOSAVE;
int save = flags & SHUTDOWN_SAVE;
int nosave = flags & SHUTDOWN_NOSAVE;
serverLog(LL_WARNING,"User requested shutdown...");
if (server.supervised_mode == SUPERVISED_SYSTEMD)
redisCommunicateSystemd("STOPPING=1\n");
/* Kill all the Lua debugger forked sessions. */
ldbKillForkedSessions();
/* Kill the saving child if there is a background saving in progress.
We want to avoid race conditions, for instance our saving child may
overwrite the synchronous saving done by SHUTDOWN. */
if (server.rdb_child_pid != -1) {
serverLog(LL_WARNING,"There is a child saving an .rdb. Killing it!");
/* Note that, in killRDBChild, we call rdbRemoveTempFile that will
* do close fd(in order to unlink file actually) in background thread.
* The temp rdb file fd may not be closed when redis exits quickly,
* but OS will close this fd when process exits. */
killRDBChild();
}
/* Kill module child if there is one. */
if (server.module_child_pid != -1) {
serverLog(LL_WARNING,"There is a module fork child. Killing it!");
TerminateModuleForkChild(server.module_child_pid,0);
}
if (server.aof_state != AOF_OFF) {
/* Kill the AOF saving child as the AOF we already have may be longer
* but contains the full dataset anyway. */
if (server.aof_child_pid != -1) {
/* If we have AOF enabled but haven't written the AOF yet, don't
* shutdown or else the dataset will be lost. */
if (server.aof_state == AOF_WAIT_REWRITE) {
serverLog(LL_WARNING, "Writing initial AOF, can't exit.");
return C_ERR;
}
serverLog(LL_WARNING,
"There is a child rewriting the AOF. Killing it!");
killAppendOnlyChild();
}
/* Append only file: flush buffers and fsync() the AOF at exit */
serverLog(LL_NOTICE,"Calling fsync() on the AOF file.");
flushAppendOnlyFile(1);
redis_fsync(server.aof_fd);
}
/* Create a new RDB file before exiting. */
if ((server.saveparamslen > 0 && !nosave) || save) {
serverLog(LL_NOTICE,"Saving the final RDB snapshot before exiting.");
if (server.supervised_mode == SUPERVISED_SYSTEMD)
redisCommunicateSystemd("STATUS=Saving the final RDB snapshot\n");
/* Snapshotting. Perform a SYNC SAVE and exit */
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (rdbSave(server.rdb_filename,rsiptr) != C_OK) {
/* Ooops.. error saving! The best we can do is to continue
* operating. Note that if there was a background saving process,
* in the next cron() Redis will be notified that the background
* saving aborted, handling special stuff like slaves pending for
* synchronization... */
serverLog(LL_WARNING,"Error trying to save the DB, can't exit.");
if (server.supervised_mode == SUPERVISED_SYSTEMD)
redisCommunicateSystemd("STATUS=Error trying to save the DB, can't exit.\n");
return C_ERR;
}
}
/* Fire the shutdown modules event. */
moduleFireServerEvent(REDISMODULE_EVENT_SHUTDOWN,0,NULL);
/* Remove the pid file if possible and needed. */
if (server.daemonize || server.pidfile) {
serverLog(LL_NOTICE,"Removing the pid file.");
unlink(server.pidfile);
}
/* Best effort flush of slave output buffers, so that we hopefully
* send them pending writes. */
flushSlavesOutputBuffers();
/* Close the listening sockets. Apparently this allows faster restarts. */
closeListeningSockets(1);
serverLog(LL_WARNING,"%s is now ready to exit, bye bye...",
server.sentinel_mode ? "Sentinel" : "Redis");
return C_OK;
}
Method 2: Shut down Redis via systemd
You can use systemd to manage the Redis service.
- Create the
redis.servicefile
[Unit]
Description=Redis In-Memory Database
Documentation=https://redis.io/docs/
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
# notify lets Redis actively notify systemd (via sd_notify) that startup finished.
Type=notify
ExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.conf --supervised systemd
# restart on failure
Restart=on-failure
RestartSec=5
# stop timeout set to 60s, allowing enough time for persistence
TimeoutStopSec=60
# start timeout set to 10 minutes, preventing timeout during heavy data loading
TimeoutStartSec=600
User=redis
Group=redis
LimitNOFILE=65536
PrivateTmp=yes
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_SYS_RESOURCE
# lower the chance of being killed by the OOM Killer
OOMScoreAdjust=-1000
# must be a relative path (relative to /run), not an absolute path
RuntimeDirectory=redis
[Install]
WantedBy=multi-user.target
- Start the Redis service with
systemctl start redis.
On startup Redis sends STATUS=Ready to accept connections to systemd and sets READY=1.
if (server.supervised_mode == SUPERVISED_SYSTEMD) {
redisCommunicateSystemd("STATUS=Ready to accept connections\n");
redisCommunicateSystemd("READY=1\n");
}
At startup Redis registers its signal handlers:
void initServer(void) {
int j;
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers(); // register signal handlers
makeThreadKillable();
// ...
}
void setupSignalHandlers(void) {
struct sigaction act;
/* When the SA_SIGINFO flag is set in sa_flags then sa_sigaction is used.
* Otherwise, sa_handler is used. */
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sigShutdownHandler;
sigaction(SIGTERM, &act, NULL);
sigaction(SIGINT, &act, NULL);
// ...
}
static void sigShutdownHandler(int sig) {
char *msg;
switch (sig) {
case SIGINT:
msg = "Received SIGINT scheduling shutdown...";
break;
case SIGTERM:
msg = "Received SIGTERM scheduling shutdown...";
break;
default:
msg = "Received shutdown signal, scheduling shutdown...";
};
/* SIGINT is often delivered via Ctrl+C in an interactive session.
* If we receive the signal the second time, we interpret this as
* the user really wanting to quit ASAP without waiting to persist
* on disk. */
if (server.shutdown_asap && sig == SIGINT) {
serverLogFromHandler(LL_WARNING, "You insist... exiting now.");
rdbRemoveTempFile(getpid(), 1);
exit(1); /* Exit with an error since this was not a clean shutdown. */
} else if (server.loading) {
serverLogFromHandler(LL_WARNING, "Received shutdown signal during loading, exiting now.");
exit(0);
}
serverLogFromHandler(LL_WARNING, msg);
server.shutdown_asap = 1; // SHUTDOWN needed ASAP
}
After receiving TERM, server.shutdown_asap = 1 is set, and serverCron calls prepareForShutdown to shut the service down—the same actions as the shutdown command. serverCron is not called directly from outside; it is registered as a time event via Redis’s internal event-driven framework (ae) and triggered periodically by the main event loop.
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// ...
/* We received a SIGTERM, shutting down here in a safe way, as it is
* not ok doing so inside the signal handler. */
if (server.shutdown_asap) {
if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
server.shutdown_asap = 0;
}
// ...
}
Check the log:
58078:C 20 Jan 2026 11:10:27.966 * Supervised by systemd. Please make sure you set appropriate values for TimeoutStartSec and TimeoutStopSec in your service unit.
58078:C 20 Jan 2026 11:10:27.966 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
58078:C 20 Jan 2026 11:10:27.966 # Redis version=7.0.15, bits=64, commit=00000000, modified=0, pid=58078, just started
58078:C 20 Jan 2026 11:10:27.966 # Configuration loaded
58078:M 20 Jan 2026 11:10:27.967 * monotonic clock: POSIX clock_gettime
58078:M 20 Jan 2026 11:10:27.969 * Running mode=standalone, port=6379.
58078:M 20 Jan 2026 11:10:27.969 # Server initialized
58078:M 20 Jan 2026 11:10:27.972 * Loading RDB produced by version 7.0.15
58078:M 20 Jan 2026 11:10:27.972 * RDB age 43530 seconds
58078:M 20 Jan 2026 11:10:27.972 * RDB memory usage when created 0.90 Mb
58078:M 20 Jan 2026 11:10:27.972 * Done loading RDB, keys loaded: 0, keys expired: 0.
58078:M 20 Jan 2026 11:10:27.972 * DB loaded from disk: 0.001 seconds
58078:M 20 Jan 2026 11:10:27.972 * Ready to accept connections
The log line Supervised by systemd. Please make sure you set appropriate values for TimeoutStartSec and TimeoutStopSec in your service unit. indicates the Redis service is managed by systemd.
- Stop the Redis service with
systemctl stop redis. Redis performs signal handling after receiving SIGTERM.
58078:signal-handler (1768878767) Received SIGTERM scheduling shutdown...
58078:M 20 Jan 2026 11:12:47.393 # User requested shutdown...
58078:M 20 Jan 2026 11:12:47.393 * Saving the final RDB snapshot before exiting.
58078:M 20 Jan 2026 11:12:47.395 * DB saved on disk
58078:M 20 Jan 2026 11:12:47.395 * Removing the pid file.
58078:M 20 Jan 2026 11:12:47.395 # Redis is now ready to exit, bye bye...
If the redis.service file configures ExecStop, that command is used to shut down Redis. If ExecStop is not configured, then when systemctl stop redis runs, systemd sends SIGTERM to the main process ($MAINPID) by default and then waits for the main process to finish handling SIGTERM before exiting.
# Sending SIGTERM lets Redis shut down gracefully (persistence, etc.)
ExecStop=/bin/kill -s TERM $MAINPID
Supplementary Knowledge
When a systemd service is configured with Type=notify, systemd waits for the service process to explicitly call sd_notify() and send the "READY=1" signal before considering the service successfully started and running.
Typical flow:
- systemd starts the service process
- The service finishes initialization (e.g. loads config, binds the port)
- Calls
sd_notify(0, "READY=1") - systemd switches the service state from
activatingtoactive (running)
"READY=1" means the service is ready (most critical!)
How it works:
- When starting a service, systemd sets a Unix domain socket path into the
NOTIFY_SOCKETenvironment variable (e.g./run/systemd/notify). sd_notify()sends a message to systemd through this socket.- systemd parses the message and updates the service state.
Redis Sentinel Mode
The shutdown command used by Redis Sentinel is the same as the one used by the main Redis process.
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"role",sentinelRoleCommand,1,"ok-loading",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"read-only no-script",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
{"auth",authCommand,2,"no-auth no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0},
{"hello",helloCommand,-2,"no-auth no-script fast",0,NULL,0,0,0,0,0}
};
In Sentinel mode, the signal handlers are registered at startup just like in Redis.
Redis 6.0 vs Redis 7.0
Compared with Redis 6.0, Redis 7.0 adds the shutdown-timeout parameter as well as shutdown-on-sigint and shutdown-on-sigterm.
################################ SHUTDOWN #####################################
# Maximum time to wait for replicas when shutting down, in seconds.
#
# During shut down, a grace period allows any lagging replicas to catch up with
# the latest replication offset before the master exists. This period can
# prevent data loss, especially for deployments without configured disk backups.
#
# The 'shutdown-timeout' value is the grace period's duration in seconds. It is
# only applicable when the instance has replicas. To disable the feature, set
# the value to 0.
#
# shutdown-timeout 10
# When Redis receives a SIGINT or SIGTERM, shutdown is initiated and by default
# an RDB snapshot is written to disk in a blocking operation if save points are configured.
# The options used on signaled shutdown can include the following values:
# default: Saves RDB snapshot only if save points are configured.
# Waits for lagging replicas to catch up.
# save: Forces a DB saving operation even if no save points are configured.
# nosave: Prevents DB saving operation even if one or more save points are configured.
# now: Skips waiting for lagging replicas.
# force: Ignores any errors that would normally prevent the server from exiting.
#
# Any combination of values is allowed as long as "save" and "nosave" are not set simultaneously.
# Example: "nosave force now"
#
# shutdown-on-sigint default
# shutdown-on-sigterm default
Empirical Notes
How long does a normal Redis shutdown take? In a test with 10 GB of data, shutdown took about 3 minutes; the exact time varies with the physical machine under test and is for reference only.
39691:signal-handler (1769503068) Received SIGTERM scheduling shutdown...
39691:S 27 Jan 2026 16:37:48.264 # User requested shutdown...
39691:S 27 Jan 2026 16:37:48.264 * Saving the final RDB snapshot before exiting.
39691:S 27 Jan 2026 16:40:40.362 * DB saved on disk
39691:S 27 Jan 2026 16:40:40.362 * Removing the pid file.
39691:S 27 Jan 2026 16:40:40.362 # Redis is now ready to exit, bye bye...
How long does Redis startup take? In a test loading an RDB of about 10 GB: the master loaded the RDB in about 18s.
535 125059:M 27 Jan 2026 16:55:50.576 # Server initialized
536 125059:M 27 Jan 2026 16:55:50.577 * Loading RDB produced by version 6.0.20
537 125059:M 27 Jan 2026 16:55:50.577 * RDB age 909 seconds
538 125059:M 27 Jan 2026 16:55:50.577 * RDB memory usage when created 12568.23 Mb
539 125059:M 27 Jan 2026 16:56:08.615 * DB loaded from disk: 18.038 seconds
The replica’s master-replica sync took about 3 minutes.
44911:S 27 Jan 2026 16:56:09.958 # Server initialized
44911:S 27 Jan 2026 16:56:09.959 * Loading RDB produced by version 6.0.20
44911:S 27 Jan 2026 16:56:09.959 * RDB age 1101 seconds
44911:S 27 Jan 2026 16:56:09.959 * RDB memory usage when created 12569.46 Mb
44911:S 27 Jan 2026 16:56:27.996 * DB loaded from disk: 18.037 seconds
44911:S 27 Jan 2026 16:56:27.996 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
44911:S 27 Jan 2026 16:56:27.996 * Ready to accept connections
44911:S 27 Jan 2026 16:56:27.996 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:56:27.996 . 0 clients connected (0 replicas), 13111588928 bytes in use
44911:S 27 Jan 2026 16:56:27.996 * Connecting to MASTER 101.66.174.21:7788
44911:S 27 Jan 2026 16:56:27.997 * MASTER <-> REPLICA sync started
44911:S 27 Jan 2026 16:56:27.997 * Non blocking connect for SYNC fired the event.
44911:S 27 Jan 2026 16:56:27.997 * Master replied to PING, replication can continue...
44911:S 27 Jan 2026 16:56:27.998 * Trying a partial resynchronization (request ece1ad4c54ae60bf15c56bad0830215be54d8bca:10486559107).
44911:S 27 Jan 2026 16:56:28.107 * Full resync from master: b055ca953ecd85a9ad8eb80b381d381b42458365:0
44911:S 27 Jan 2026 16:56:28.107 * Discarding previously cached master state.
44911:S 27 Jan 2026 16:56:33.009 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
// ...
44911:S 27 Jan 2026 16:59:18.494 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:20.310 * MASTER <-> REPLICA sync: receiving 10485916109 bytes from master to disk
44911:S 27 Jan 2026 16:59:23.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:23.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:28.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:28.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:33.499 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:33.499 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:38.515 - DB 4: 1000 keys (0 volatile) in 1024 slots HT.
44911:S 27 Jan 2026 16:59:38.515 . 0 clients connected (0 replicas), 13111568192 bytes in use
44911:S 27 Jan 2026 16:59:38.888 * MASTER <-> REPLICA sync: Flushing old data
44911:S 27 Jan 2026 16:59:38.900 * MASTER <-> REPLICA sync: Loading DB in memory
44911:S 27 Jan 2026 16:59:38.954 * Loading RDB produced by version 6.0.20
44911:S 27 Jan 2026 16:59:38.954 * RDB age 190 seconds
44911:S 27 Jan 2026 16:59:38.954 * RDB memory usage when created 12568.22 Mb
44911:S 27 Jan 2026 16:59:56.914 * MASTER <-> REPLICA sync: Finished with success