Skip to main content
When Ryvn performs a rolling deploy or scales down an installation, it sends a SIGTERM signal to each instance being replaced. If your application doesn’t handle this signal, the process is force-killed after the termination grace period — any in-flight work (running workflows, processing queue messages, active requests) is lost. This guide walks through configuring graceful shutdown so your workers finish their current work before exiting. While the examples use Temporal workers, the same pattern applies to any long-running process — RabbitMQ consumers, background job processors, or custom queue workers.

How it works

During a rolling deploy, Ryvn starts the new instance first. Once it’s ready, Ryvn sends SIGTERM to the old instance. Your worker catches that signal, stops polling for new tasks, and drains any in-flight work before exiting cleanly. Ryvn waits up to terminationGracePeriodSeconds before force-killing the instance.

Step 1: Handle SIGTERM in your worker

Your application needs to listen for SIGTERM and initiate a clean shutdown. The key behavior is:
  1. Stop accepting new work — stop polling queues or accepting new requests
  2. Finish in-flight work — let running tasks, workflows, or activities complete
  3. Exit cleanly — exit the process once all work is drained
Temporal’s Worker class has a built-in shutdown() method that stops the worker from picking up new tasks and allows in-flight workflows and activities to complete. The worker.run() promise resolves once the worker has fully drained.
worker.ts
import { Worker } from '@temporalio/worker';

async function main() {
  const worker = await Worker.create({
    // ... your worker config
    taskQueue: 'my-queue',
    shutdownGraceTime: '30s',
  });

  // Trigger shutdown on SIGTERM / SIGINT
  const shutdown = () => worker.shutdown();
  process.on('SIGTERM', shutdown);
  process.on('SIGINT', shutdown);

  // worker.run() resolves once shutdown completes and all work is drained
  await worker.run();

  // All in-flight work is done — exit cleanly
  process.exit(0);
}

main().catch((err) => {
  console.error('Worker failed', err);
  process.exit(1);
});
worker.shutdown() is a void method that signals the worker to stop. It does not return a promise. The worker.run() promise resolves once shutdown is complete and all in-flight work has drained.

Step 2: Ensure the process receives SIGTERM

A common pitfall is wrapping your application in a shell script or bash -c "..." in your Dockerfile. When this happens, bash is PID 1 and your application is a child process — SIGTERM is sent to bash, which does not forward it to child processes by default.
If your Dockerfile uses CMD ["bash", "-c", "..."], CMD npm start, or even CMD ["npm", "start"], your application process may never receive SIGTERM. Shell wrappers swallow the signal, and npm does not forward signals to its child process. This means your graceful shutdown handler will never run.
There are two ways to fix this:

Step 3: Set terminationGracePeriodSeconds

By default, Ryvn gives your instance 30 seconds to shut down after receiving SIGTERM. If your workers run tasks that take longer than 30 seconds, you need to increase this value to match your longest expected task duration.
Navigate to Environments → select your environment → select the installation → SettingsValues, then set terminationGracePeriodSeconds in your configuration.
Set terminationGracePeriodSeconds to at least the wall-clock time of your longest-running task. For example, if your longest workflow activity takes 5 minutes, set it to 300 or higher.

Autoscaling considerations

If you’re using autoscaling with Temporal or RabbitMQ triggers, the same graceful shutdown pattern applies during scale-down events. When autoscaling reduces the replica count, Ryvn terminates excess instances with SIGTERM — your workers need to drain their in-flight work before exiting. The SIGTERM handler ensures that scale-down events don’t kill active work. Combined with an appropriate terminationGracePeriodSeconds, workers will finish their current tasks even as the installation scales down.
# yaml-language-server: $schema=https://api.ryvn.app/v1/schemas/resources.json
kind: ServiceInstallation
metadata:
  name: temporal-worker
spec:
  service: worker
  environment: production
  autoscaling:
    enabled: true
    minReplicas: 1
    maxReplicas: 10
    triggers:
      - temporal:
          connection: temporal-prod
          taskQueue: worker-queue
          targetQueueSize: 5
  config: |
    terminationGracePeriodSeconds: 300
    resources:
      requests:
        cpu: 0.5
        memory: 512Mi

Checklist

Before deploying, verify the following:
1

Signal handler

Your worker code listens for SIGTERM and initiates a clean shutdown — stops accepting new work and drains in-flight tasks.
2

Process receives signals

Your Dockerfile uses exec form (CMD ["node", "..."]) or tini so that your application process is PID 1 and receives SIGTERM directly.
3

Grace period is sufficient

terminationGracePeriodSeconds in your Ryvn installation config is set to at least the duration of your longest-running task.
4

Test locally

Send SIGTERM to your worker process locally (kill -TERM <pid>) and verify it drains cleanly before exiting.

Common issues

Your process likely isn’t receiving SIGTERM. Check your Dockerfile — if you’re using bash -c, the signal goes to bash, not your app. Switch to exec form or use tini. See Step 2.
Your terminationGracePeriodSeconds may be too short. Ryvn force-kills the instance after this period. Increase it to exceed your longest task duration. See Step 3.
Your shutdown handler may not be calling process.exit() after draining. For Temporal workers, make sure you call process.exit(0) after worker.run() resolves. For custom workers, ensure your drain-check logic eventually exits.
If your instance runs multiple worker processes, each one needs its own SIGTERM handler. Consider using tini as an init process so signals are forwarded to all children, or structure your code to shut down all workers from a single signal handler.