
Table of Contents
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.
Deno.addSignalListener to stop accepting new connections while draining active requests, combined with separate HTTP endpoints for liveness and readiness that reflect actual dependency status rather than just process uptime.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 Type | Purpose | Failure Action | Check Dependencies? | Response During Shutdown |
|---|---|---|---|---|
| Liveness | Detect deadlocks, frozen event loops | Kill & restart container | No (local only) | 200 OK (until SIGKILL) |
| Readiness | Traffic acceptance capability | Remove from Service endpoints | Yes (DB, Cache, Queue) | 503 Service Unavailable |
| Startup | Slow initialization verification | Kill if timeout exceeded | Optional | N/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.
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.
Recommended Probe Configuration
- 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.
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:
- Ignoring SIGINT: Local development and some orchestrators send SIGINT instead of SIGTERM. Always register listeners for both signals to ensure consistent behavior across environments.
- 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.
- Health Check Authentication: Placing health endpoints behind global auth middleware causes probe failures. Whitelist
/health/*paths explicitly in your router configuration. - 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.
- 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.