Graceful Shutdown in Databases: Signal Handling and Concurrency (Redis Internals & Go Implementation)

Arpit Bhayani

Arpit Bhayani

Jun 06, 2026 • 9 min read

Play

Note: This article is an AI-generated write-up based on the captions and transcript of the video above. Watch the embedded video for the full visual walk-through and nuances.

Graceful Shutdown in Databases: Signal Handling and Concurrency (Redis Internals & Go Implementation)

Introduction to Graceful Shutdown

In the world of long-running processes, especially databases, ensuring a “graceful shutdown” is paramount. A graceful shutdown means that when a process is terminated, it performs necessary cleanup, persists any unsaved data, and completes any ongoing operations before finally exiting. This contrasts sharply with an abrupt termination, which can lead to data loss, corrupted files, and a poor user experience.

Why Graceful Shutdown is Crucial

  1. Data Integrity and Consistency: Databases often hold data in memory (buffers, caches) that needs to be flushed to disk. For instance, Redis creates a final RDB snapshot during shutdown to ensure all in-memory data is persisted. Without this, recent writes could be lost.
  2. Resource Cleanup: Processes acquire various system resources like network sockets, file handles, and temporary files. A graceful shutdown ensures these resources are properly released. For example, unbinding a socket allows other processes to use that port immediately.
  3. Client Experience: If a server abruptly terminates while processing a client’s request, the client receives an error, leading to a poor experience. Graceful shutdown ensures that existing client requests are completed before the server goes offline.

Understanding Operating System Signals

Operating systems communicate important events to processes through signals. These are notifications sent by the kernel to a process, indicating that something significant has occurred. Processes can “trap” these signals and execute custom handlers.

Signals for Graceful Termination

Two primary signals are used to request a graceful shutdown:

  • SIGINT (Interrupt Signal): Typically sent when a user presses Ctrl+C in the terminal.
  • SIGTERM (Termination Signal): The default signal sent by the kill command, requesting a process to terminate gracefully.

When Redis receives SIGINT or SIGTERM, it initiates a sequence of actions:

  1. Clean up temporary files.
  2. Flush file buffers and close sockets.
  3. Persist data: For Redis, this involves saving a final RDB snapshot to disk.
  4. Wait for active commands: If a client command is currently executing (especially critical for single-threaded databases like Redis), the server waits for its completion to avoid data inconsistency or client errors.

Signals for Critical Errors (Debugging)

Some signals indicate severe, unexpected errors within the process. While these often lead to termination, a graceful handler can capture crucial debugging information:

  • SIGSEGV (Segmentation Fault): Occurs when a process tries to access a memory location it doesn’t own (e.g., accessing memory outside its allocated segment).
  • SIGBUS (Bus Error): Similar to SIGSEGV, but typically indicates an invalid physical address access (e.g., accessing memory beyond physical RAM limits).
  • SIGFPE (Floating Point Exception): Raised for arithmetic errors like division by zero or integer overflow.
  • SIGILL (Illegal Instruction): Occurs when the CPU attempts to execute an instruction that is not valid or recognized.

Upon receiving any of these critical signals, Redis captures extensive debugging information, such as the stack trace, the state of registers, and client information, and flushes it to disk. This allows developers to diagnose and fix the underlying issues, making the database more resilient.

Ignored Signals

Some signals are typically ignored by long-running server processes to ensure continuous operation:

  • SIGHUP (Hangup): Sent when a terminal session is disconnected. Servers often ignore this to continue running in the background.
  • SIGPIPE (Broken Pipe): Occurs when a process tries to write to a pipe or socket whose reading end has been closed. Ignoring this prevents the server from crashing due to client disconnections.

Redis’s Internal Implementation of Signal Handling

To understand how Redis handles signals, we can trace its source code. The entry point for server initialization is initServer in server.c. Within this function, setupSignalHandlers is invoked. This function uses the sigaction system call (a standard C library function for signal management) to register custom handlers for various signals. By examining server.c, one can see how Redis explicitly sets up handlers for SIGTERM, SIGINT, SIGSEGV, SIGBUS, SIGFPE, SIGILL, and SIGABRT, while ignoring SIGHUP and SIGPIPE.

Implementing Graceful Shutdown in Go (DiceDB Example)

Let’s consider implementing a graceful shutdown mechanism in a Go-based database, similar to DiceDB.

Core Components

  1. Signal Channel: Go’s os/signal package provides a convenient way to listen for OS signals. We create a channel of type os.Signal and use signal.Notify to register it for SIGINT and SIGTERM.
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  2. Goroutines: We separate the main server logic from the signal handling logic into distinct goroutines:
    • runAsyncTCPServer: Handles accepting connections and processing client commands.
    • waitForSignal: Dedicated to listening for and reacting to OS signals.
  3. sync.WaitGroup: The main function uses a sync.WaitGroup to wait for both goroutines to complete before exiting, ensuring that the shutdown process is fully managed.
  4. engineStatus: A shared atomic integer variable (atomic.Int32) is introduced to track the server’s operational state. This is crucial for coordinating between the server logic and the signal handler.
    • Waiting: The server is idle, waiting for I/O (e.g., blocked on epoll_wait).
    • Busy: The server is actively processing a client command.
    • ShuttingDown: The server has received a shutdown signal and is in the process of terminating.

The waitForSignal Goroutine Logic

This goroutine is responsible for orchestrating the shutdown:

func waitForSignal(wg *sync.WaitGroup, sigChan <-chan os.Signal, engineStatus *atomic.Int32) {
    defer wg.Done()

    // Block until a signal is received
    <-sigChan 
    log.Println("Received shutdown signal. Initiating graceful shutdown...")

    // Wait for any currently executing command to complete
    for engineStatus.Load() == Busy {
        time.Sleep(100 * time.Millisecond) // Polling, could use condition variable for efficiency
    }

    // Set status to ShuttingDown to prevent new commands
    engineStatus.Store(ShuttingDown)

    // Perform final shutdown tasks (e.g., data persistence)
    core.Shutdown() // e.g., BG_REWRITE_AOF

    log.Println("Database saved. Exiting.")
    os.Exit(0)
}

Modifying the Main Server Loop (runAsyncTCPServer)

  1. Loop Condition: The server’s main processing loop, which typically runs infinitely, is modified to check engineStatus != ShuttingDown. This allows the loop to terminate once a shutdown is initiated.
  2. State Transitions: The runAsyncTCPServer goroutine manages the Waiting and Busy states:
    • Before epoll_wait (or equivalent I/O multiplexing call): The server sets engineStatus to Waiting because it’s idle, waiting for incoming connections or data.
    • After epoll_wait returns (I/O ready): The server attempts to transition engineStatus to Busy because it’s about to process a command.
    • After command execution completes: The server reverts engineStatus back to Waiting.

The Critical Concurrency Edge Case: Preventing Re-entry to Busy State

One of the most subtle and critical edge cases in concurrent graceful shutdown involves preventing the server from accepting new work after a shutdown has been initiated but before it has fully exited.

The Problem: Imagine the following sequence:

  1. The waitForSignal goroutine receives SIGINT.
  2. It checks engineStatus. If engineStatus is Waiting, it proceeds to set it to ShuttingDown.
  3. However, just before waitForSignal can set engineStatus to ShuttingDown, the runAsyncTCPServer goroutine (which was in Waiting state) successfully completes an epoll_wait call, receives a new client request, and attempts to transition engineStatus to Busy.
  4. If this transition to Busy succeeds, the waitForSignal goroutine might then set engineStatus to ShuttingDown while a new command is already executing. This would lead to the abrupt termination of that new command, which is exactly what graceful shutdown aims to prevent.

The Solution: Atomic Compare-and-Swap (CAS) To prevent this race condition, the transition from Waiting to Busy must be atomic and conditional. We use atomic.CompareAndSwap (or atomic.CompareAndSwapInt32 in Go) when attempting to set engineStatus to Busy:

// In runAsyncTCPServer, after epoll_wait succeeds and before processing a command:

// Atomically try to change status from Waiting to Busy
// If the status is already ShuttingDown, this will fail.
if !atomic.CompareAndSwapInt32(engineStatus, Waiting, Busy) {
    // If CAS failed, it means engineStatus was not Waiting (e.g., it was ShuttingDown)
    // In this case, we should not proceed with processing the command.
    log.Println("Server is shutting down, not accepting new commands.")
    // Close the new connection or handle it appropriately without processing
    return // Exit the current command processing path
}

// ... proceed to process the command (now engineStatus is Busy)

// After command execution completes:
engineStatus.Store(Waiting) // Revert to Waiting

This CompareAndSwap operation ensures that engineStatus can only transition from Waiting to Busy. If engineStatus has already been set to ShuttingDown by the waitForSignal goroutine, the CompareAndSwap will fail, and the server will immediately stop processing the new request, preventing any commands from starting after the shutdown process has begun. This guarantees that only commands already in progress when the signal was received are allowed to complete.

Demonstration

To illustrate the effectiveness of this graceful shutdown, a SLEEP command can be introduced into the database. This command simply pauses execution for a specified number of seconds.

  1. Normal Ctrl+C: When the server is idle and Ctrl+C is pressed, the waitForSignal goroutine immediately detects engineStatus as Waiting, sets it to ShuttingDown, performs the AOF rewrite, and exits gracefully.
  2. SLEEP 10 Command:
    • A client connects and issues SLEEP 10.
    • The runAsyncTCPServer goroutine sets engineStatus to Busy and starts executing the SLEEP command.
    • While SLEEP 10 is running, Ctrl+C is pressed.
    • The waitForSignal goroutine receives the signal. It enters its loop, continuously checking engineStatus. Since engineStatus is Busy, it waits.
    • After 10 seconds, the SLEEP command completes, and runAsyncTCPServer sets engineStatus back to Waiting.
    • The waitForSignal goroutine immediately detects the Waiting state, sets engineStatus to ShuttingDown, performs the AOF rewrite, and then exits.

This demonstration clearly shows that the server waits for the existing SLEEP command to finish before initiating its final shutdown sequence, ensuring no client requests are abruptly terminated.

Conclusion

Implementing graceful shutdown is a fundamental aspect of building robust and reliable distributed systems, especially databases. It involves careful handling of operating system signals, thoughtful state management, and precise coordination between concurrent execution paths. The use of atomic operations like CompareAndSwap is crucial for managing critical state transitions and preventing race conditions that could undermine the graceful termination process. By understanding and applying these principles, developers can ensure their systems maintain data integrity, clean up resources effectively, and provide a consistent experience even during shutdown events.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses