Graceful Shutdown and Health Checks in Deno

Khimananda Oli 8 min read Programming and Languages
Graceful Shutdown and Health Checks in Deno

By Khimananda Oli | Last reviewed: August 2026

Dropped requests during rolling updates remain a primary source of user-facing errors in modern web services. Implementing graceful shutdown and health checks in Deno solves this by coordinating application lifecycle events with infrastructure orchestrators like Kubernetes or Docker Swarm. Without explicit signal handling and distinct readiness probes, load balancers continue routing traffic to terminating instances, causing intermittent 502/503 errors that erode trust. This guide provides the exact implementation patterns I use in production to ensure clean connection draining and accurate service discovery.

Running StateAccepting TrafficSIGTERM ReceivedStop Accept / DrainActive Reqs DoneClose DB / CacheProcess ExitCode 0Readiness → False
Lifecycle flow for graceful shutdown and health checks in Deno: signal reception triggers drain before exit

How do you handle SIGTERM for graceful shutdown in Deno?

The foundation of graceful shutdown and health checks in Deno is proper signal interception. Unlike Node.js, where process.on('SIGTERM') is standard, Deno uses Deno.addSignalListener. A common mistake in 2026 is still using the deprecated Deno.signal() async iterator API; always prefer the listener callback pattern for synchronous setup and cleaner teardown logic. When your container orchestrator sends SIGTERM, your application must immediately stop accepting new connections but continue processing in-flight requests until they complete or a timeout expires.

Implementing the Signal Listener

You need a mechanism to track active connections and coordinate server closure. The following pattern uses a promise-based coordination system that integrates directly with Deno.serve. This approach avoids race conditions between the signal handler and the HTTP server loop.

const controller = new AbortController();
let activeConnections = 0;
let isShuttingDown = false;

// Track connection state manually if not using middleware
const trackConnection = (handler) => async (request, info) => {
  if (isShuttingDown) {
    return new Response("Service Unavailable", { status: 503 });
  }
  activeConnections++;
  try {
    return await handler(request, info);
  } finally {
    activeConnections--;
  }
};

Deno.addSignalListener("SIGTERM", () => {
  console.log("SIGTERM received. Starting graceful shutdown...");
  isShuttingDown = true;
  
  // Stop accepting new connections
  controller.abort();
  
  // Wait for active connections with timeout
  const maxWaitMs = 30000;
  const startTime = Date.now();
  
  const checkInterval = setInterval(() => {
    if (activeConnections === 0 || (Date.now() - startTime > maxWaitMs)) {
      clearInterval(checkInterval);
      console.log(`Shutdown complete. Remaining connections: ${activeConnections}`);
      Deno.exit(0);
    }
  }, 100);
});

const server = Deno.serve(
  { port: 8000, signal: controller.signal },
  trackConnection((req) => new Response("Hello World"))
);

This implementation ensures that once SIGTERM arrives, the AbortController signals the HTTP server to stop binding new sockets. Existing requests tracked via the wrapper continue to completion. For teams managing complex database pools alongside HTTP servers, integrating this pattern with PostgreSQL administration essentials ensures connections are returned to the pool before the process exits, preventing orphaned transactions on the database side.

What is the difference between liveness and readiness probes in Deno?

Many engineers conflate health endpoints, leading to cascading failures during deployments. In the context of graceful shutdown and health checks in Deno, distinguishing between liveness and readiness is non-negotiable for Kubernetes stability. Liveness answers "Is the process deadlocked?" while readiness answers "Can this instance serve traffic right now?" Restarting a pod because it is temporarily busy (false positive liveness failure) is far worse than simply removing it from the load balancer rotation.

Probe TypePurposeFailure ActionCheck Dependencies?Response During Shutdown
LivenessDetect deadlocks, frozen event loopsKill & restart containerNo (local only)200 OK (until SIGKILL)
ReadinessTraffic acceptance capabilityRemove from Service endpointsYes (DB, Cache, Queue)503 Service Unavailable
StartupSlow initialization verificationKill if timeout exceededOptionalN/A (runs once)

Designing Dependency-Aware Readiness Checks

Your readiness endpoint must verify downstream connectivity. A simple "OK" response is insufficient for production systems. If your Deno service depends on Redis and Postgres, the readiness check should attempt a lightweight ping to both. However, avoid heavy queries; a SELECT 1 or PING command is sufficient. Crucially, during the shutdown phase initiated by SIGTERM, your readiness endpoint must immediately return 503 to signal the load balancer to stop sending traffic before you finish draining existing requests.

HTTP RequestRoute: /health/*/health/liveCheck: Event LoopAction: Restart Pod/health/readyCheck: DB + CacheAction: Remove Traffic200 OK (Always)200 / 503 Dynamic
Decision tree separating liveness and readiness probes for Deno applications

How do you implement health check endpoints in Deno.serve?

Integrating health checks into Deno.serve requires routing logic that executes before your main business middleware. In 2026, most Deno teams use lightweight routers or native URL parsing. Regardless of the framework, health endpoints must be fast, side-effect free, and exempt from authentication middleware. Never put your health checks behind JWT validation or rate limiting; infrastructure probes do not carry user tokens.

Production-Ready Health Router

The following example demonstrates a minimal, dependency-aware health check implementation compatible with vanilla Deno. It includes a timeout guard to prevent the readiness probe itself from hanging if a downstream service is unresponsive.

async function checkDependency(name, checkFn, timeoutMs = 2000) {
  try {
    await Promise.race([
      checkFn(),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error("Timeout")), timeoutMs)
      )
    ]);
    return { name, status: "up" };
  } catch (err) {
    return { name, status: "down", error: err.message };
  }
}

async function readinessHandler() {
  if (isShuttingDown) {
    return new Response(JSON.stringify({ status: "shutting_down" }), {
      status: 503,
      headers: { "Content-Type": "application/json" }
    });
  }

  const checks = await Promise.all([
    checkDependency("postgres", () => db.ping()),
    checkDependency("redis", () => redis.ping())
  ]);

  const allUp = checks.every(c => c.status === "up");
  return new Response(JSON.stringify({ status: allUp ? "ok" : "degraded", checks }), {
    status: allUp ? 200 : 503,
    headers: { "Content-Type": "application/json" }
  });
}

// In your main handler:
if (url.pathname === "/health/live") return new Response("OK");
if (url.pathname === "/health/ready") return await readinessHandler();

This pattern aligns with observability best practices discussed in the four golden signals of monitoring. By exposing dependency status in JSON format, you enable Prometheus exporters or OpenTelemetry collectors to scrape granular availability metrics without additional instrumentation overhead.

How do you configure Kubernetes probes for Deno applications?

Writing the code is only half the battle; configuring Kubernetes to respect your graceful shutdown and health checks in Deno completes the zero-downtime chain. Misconfigured probe timings are the most frequent cause of deployment failures I see in audits. The key is aligning K8s timings with your application's actual drain duration. If your app takes up to 30 seconds to drain, but your terminationGracePeriodSeconds is set to the default 30s, you risk SIGKILL before cleanup finishes. Always set the grace period higher than your maximum expected drain time.

  • initialDelaySeconds: Set based on cold start time. For Deno, typically 3–5s unless loading large ML models.
  • periodSeconds: 10s is standard. Lower values increase control plane load unnecessarily.
  • failureThreshold: 3 for liveness (allows transient blips), 2 for readiness (fails fast to protect users).
  • timeoutSeconds: Must exceed your dependency check timeout. If readiness checks take 2s, set this to 3s minimum.
livenessProbe:
  httpGet:
    path: /health/live
    port: 8000
  initialDelaySeconds: 5
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8000
  initialDelaySeconds: 3
  periodSeconds: 5
  failureThreshold: 2
  timeoutSeconds: 3
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]

Note the preStop hook above. This is critical. Kubernetes removes the pod from endpoints and sends SIGTERM simultaneously. Due to asynchronous propagation, traffic may still arrive for a few seconds after SIGTERM. The 5-second sleep in preStop acts as a buffer, allowing the endpoint removal to propagate across all kube-proxies before your application begins refusing connections. This technique, combined with the patterns in blue-green and canary deploys on Kubernetes, virtually eliminates transition errors.

t=0st=35sDelete SignalPreStop Hook (Sleep 5s)Endpoint Removal PropagatesSIGTERM + Drain Active ReqsReadiness Returns 503Clean Exit (0)SIGKILL(Fallback)Total Grace Period: 35s Buffer
Kubernetes termination timeline showing preStop buffer and SIGTERM drain window

Common Pitfalls in Deno Lifecycle Management

Even with correct code, subtle misconfigurations break graceful shutdown and health checks in Deno. Avoid these frequent issues observed in production environments:

  1. Ignoring SIGINT: Local development and some orchestrators send SIGINT instead of SIGTERM. Always register listeners for both signals to ensure consistent behavior across environments.
  2. Blocking the Event Loop: Heavy synchronous cleanup in the signal handler prevents the HTTP server from finishing pending responses. All cleanup must be async or deferred.
  3. Health Check Authentication: Placing health endpoints behind global auth middleware causes probe failures. Whitelist /health/* paths explicitly in your router configuration.
  4. Missing Timeout on Drain: Without a maximum wait time, a single stuck request can prevent pod termination indefinitely, blocking deployments. Always enforce a hard deadline.
  5. Verbose Logging During Shutdown: Excessive logging during drain can overwhelm log aggregators. Reduce log level to WARN/ERROR once shutdown initiates to keep audit trails clean.

Reliable Deployments Start with Lifecycle Awareness

Mastering graceful shutdown and health checks in Deno transforms your deployment pipeline from a source of intermittent errors into a predictable, boring operation. The combination of proper signal handling, distinct probe semantics, and Kubernetes-aligned timing creates a resilient foundation for any production service. Remember that reliability is not a feature you add later; it is architected into the lifecycle from day one. If your team needs assistance auditing your Deno deployment strategy or implementing compliance-ready infrastructure patterns, reach out to discuss your architecture.

Frequently Asked Questions

Listen for SIGINT and SIGTERM signals using Deno.addSignalListener. Close active database connections, flush logs, and stop accepting new requests before calling Deno.exit(0) to ensure clean process termination without data loss or corrupted state during deployments.

Expose a GET /health endpoint returning 200 OK when dependencies are reachable. Return 503 Service Unavailable if critical services like databases fail. Keep this endpoint lightweight and free of authentication to support load balancer probes and Kubernetes liveness checks effectively.

No, Deno does not auto-shutdown on SIGTERM. You must explicitly register signal listeners to catch termination signals from Docker or Kubernetes. Without custom handlers, the runtime terminates immediately, potentially dropping active requests and leaving transactions incomplete during container scaling events.

Set grace periods between 15 and 30 seconds to match your cloud provider's default termination window. Ensure all cleanup tasks complete within this timeframe. Configure your orchestrator's terminationGracePeriodSeconds to exceed your application's maximum expected drain duration by a safe margin.

Yes, pass an AbortSignal to Deno.serve and fetch calls. Trigger abort() during your signal handler to stop accepting new connections and cancel pending operations. This integrates natively with the web-standard API and simplifies coordinating shutdown across multiple async resources.

Verify the probe path matches your route exactly and returns valid HTTP status codes. Check that the port in your manifest aligns with Deno.serve. Ensure readiness probes wait for database connections to establish before marking the pod as ready to receive traffic.

Track open sockets in a Set and close them gracefully upon receiving termination signals. Send a close frame with code 1001 to notify clients. Wait for confirmation or enforce a timeout before forcing closure to prevent abrupt disconnects during rolling updates.

Generally no, as external outages would cascade into self-inflicted downtime. Only include dependencies your service cannot function without. Use separate dependency-specific endpoints for deep checks while keeping the primary health route fast and isolated from third-party latency or failures.

Write structured JSON logs during signal handling before closing streams. Flush buffers synchronously or await async writes with a timeout. Avoid console.log after initiating exit as output may truncate. Use dedicated logging libraries that support synchronous flushing for reliable audit trails.

The orchestrator sends SIGKILL, forcefully terminating the process without further cleanup. Implement timeouts on all shutdown tasks to guarantee completion within the allocated window. Prioritize critical operations like transaction rollbacks over non-essential tasks like metrics flushing to avoid data corruption.

Yes, Deno.serve accepts an AbortSignal and supports onListen callbacks for startup coordination. Combine it with signal listeners to stop accepting requests while allowing in-flight handlers to complete. This built-in integration eliminates the need for third-party HTTP server wrappers in 2026.

Run your Deno app and send kill -SIGTERM from another terminal. Observe logs for cleanup execution and verify no new requests are accepted. Use curl to confirm the health endpoint returns 503 during draining and that existing connections close cleanly.

Not necessarily. Define the health route directly in your router or handler map before other routes for fastest matching. Middleware adds overhead; reserve it for cross-cutting concerns like logging. Keep health endpoints simple and dependency-light to minimize false negatives under load.

Deno uses web-standard AbortController and native signal APIs instead of process.on. It lacks cluster module complexity but offers first-class TypeScript and secure defaults. Both require explicit signal handling, though Deno's integrated toolchain reduces boilerplate for implementing consistent shutdown behavior across environments.

Unauthenticated health endpoints can leak infrastructure details via verbose error messages. Restrict responses to status codes only in production. Place health routes behind internal network policies when possible. Never expose stack traces, versions, or dependency states that could aid reconnaissance attacks against your deployment.