+1 (850) 665-2441

Zero-Downtime Deploys for Camel: Graceful Shutdown, Health Checks, and Rolling Restarts

Most Camel outages we get called about are not caused by bad routes. They are caused by deploys. A pod gets a SIGTERM, the JVM goes away in two seconds, and the forty exchanges that were mid-flight become a support ticket: a half-written file on an SFTP server, a JMS message redelivered into a non-idempotent endpoint, an HTTP caller holding a socket that closed under it.

Camel has the machinery to make this boring. It is on by default, the defaults are not quite right for most estates, and the pieces — graceful shutdown, health checks, and the route controller — only work as a system when they agree with each other and with your orchestrator.

What graceful shutdown actually does

On shutdown, Camel's ShutdownStrategy does two things in order. It stops the consumers first, so no new exchanges enter the routes. Then it waits for in-flight exchanges to finish, up to a timeout (300 seconds by default), before forcing the context down.

The order matters, and so does the default ShutdownRunningTask setting: CompleteCurrentTaskOnly. For a file or SFTP poll that picked up 500 files in one batch, that means Camel finishes the current file and abandons the rest of the batch. That is usually what you want — the next instance will poll them again. CompleteAllTasks finishes the whole batch instead, which is right for a batch that must not be split and wrong for anything that could hold shutdown open for an hour.

Two settings worth being explicit about rather than inheriting:

camel.main.shutdown-timeout = 45
camel.main.shutdown-log-inflight-exchanges-on-timeout = true

The timeout should be shorter than your orchestrator's grace period, not longer. In Kubernetes, terminationGracePeriodSeconds defaults to 30; a Camel shutdown timeout of 300 means the kubelet sends SIGKILL 270 seconds before Camel was planning to give up, and every protection above is decoration. Pick a real number — how long does your slowest legitimate exchange take? — set Camel below it and the grace period above it.

The in-flight logging is the other half. When shutdown does time out, you want the log line naming the route and exchange that refused to finish, because that is the route with the unbounded downstream call you have not found yet.

Route startup order and the suspend step

For interactive routes — a REST DSL endpoint behind a load balancer — stopping consumers at SIGTERM is too late. The balancer still has you in rotation, and the next request lands on a closed port. The fix is not a Camel feature, it is sequencing: fail the readiness probe first, let the balancer drain, then let Camel shut down. In Kubernetes that is usually a preStop sleep of a few seconds; the pod leaves the endpoints list, traffic stops arriving, and only then does the JVM start its own shutdown.

For pull-based routes — JMS, Kafka, file — you do not need the sleep. Nobody routes traffic to you; you go and fetch it. Stopping consumers is sufficient and immediate.

suspend is the underused middle gear. Suspending a context stops consumers but leaves the context alive, which is what you want for a maintenance pause or for draining before a controlled restart. Shutdown is for leaving; suspend is for pausing.

Health checks that mean something

Camel exposes health checks through camel-health, surfaced in Spring Boot as actuator groups and in Quarkus as SmallRye endpoints. The distinction that matters operationally:

  • Liveness answers "is this JVM broken beyond recovery?" It should be close to a heartbeat. Wiring route status into liveness is how you get a restart loop: a downstream broker goes down, every consumer fails, Kubernetes kills the pod, the new pod cannot connect either, and now you have a crash-looping deployment on top of a broker outage.
  • Readiness answers "should traffic come here right now?" This is where route and consumer status belongs. A pod whose JMS consumers are disconnected should leave the rotation for HTTP traffic, and a pod still starting its routes should not be declared ready.

Camel's built-in checks cover context status and consumer status per route. You can exclude a route from readiness when its health is genuinely optional — a nightly reconciliation route should not take an API pod out of rotation.

Write your own check for the condition that actually predicts an incident in your estate: a credential that has expired, a mandatory lookup cache that never loaded, a required queue that does not exist. A custom HealthCheck is a small class and it tells the truth earlier than a dashboard will.

The supervising route controller

By default, a route that fails to start takes the whole application with it. That is correct for a single-purpose service and often wrong for a Camel app running thirty routes, where one unreachable partner SFTP server should not keep the other twenty-nine offline.

The supervising route controller changes that: routes that fail to start are retried with backoff while the rest of the context runs.

camel.main.route-controller-supervise-enabled = true
camel.main.route-controller-initial-delay = 5s
camel.main.route-controller-backoff-delay = 10s
camel.main.route-controller-backoff-max-attempts = 20

The trade-off is honest and worth stating out loud: you have traded a loud failure (the app will not start) for a quiet one (the app is up, one route is not). That is only an improvement if readiness reflects it and somebody is alerted. Supervised routes without monitoring is how an interface goes dark for three days and the first report comes from the business.

In-flight is not the same as not-lost

Graceful shutdown protects exchanges that are in the route. It does nothing for the work sitting in a SEDA queue or a bounded aggregator's in-memory repository when the JVM exits. If losing that work matters, the queue needs to be a broker and the aggregation repository needs to be persistent — JDBC or similar. No shutdown timeout will save an in-memory buffer; it will only wait politely while it disappears.

Same for transactional consumers. A JMS consumer that is killed mid-transaction will have its message redelivered, which is correct behavior and exactly why the idempotency work matters. Graceful shutdown reduces redelivery; it does not eliminate it.

A short checklist

  • Camel shutdown timeout set deliberately, and below the orchestrator grace period.
  • ShutdownRunningTask chosen per route, not inherited, for batch-polling consumers.
  • preStop drain for HTTP-facing pods; nothing needed for pull-based consumers.
  • Readiness carries route and consumer status; liveness does not.
  • At least one custom health check for the failure mode specific to your estate.
  • Supervising route controller on where partial availability beats none — with alerting attached.
  • In-memory queues and aggregation repositories made durable, or accepted as lossy in writing.

None of this is clever. It is the difference between a deploy nobody notices and a deploy that generates reconciliation work, and it is usually a morning's configuration on an estate that has been living without it for years.