How do I do graceful shutdown on SIGTERM during autoscaling scale-in?
Question
On AWS, Autoscaling spins up new EC2 instances when CPU goes above 70% and shuts some down when it drops below 30% (scale-in). When an instance is terminated, long-running HTTP requests or queue jobs in flight get cut off mid-way and leave inconsistency behind. How do I make the app (Laravel Octane / Go) catch `SIGTERM`, finish the in-flight requests, and shut down gracefully by refusing new ones?
Answer
Short answer: scale-in cuts work off mid-flight because the app doesn’t drain on SIGTERM — what you need to fix is the process’s shutdown lifecycle.
Short answer
The real issue isn’t autoscaling, it’s the shutdown protocol: when the platform sends SIGTERM, your app either dies instantly or gets cut off before finishing. The right order is: stop the traffic first, finish in-flight work next, die last. I covered why Octane’s persistent-process model changes this behaviour in the Laravel Octane post.
Why
-
Shutdown is a protocol, not an event. Three things have to happen when the “die” signal arrives, and the order matters; skip two and go straight to the third and you corrupt data.
-
The platform’s patience is finite. SIGKILL follows SIGTERM once the grace period expires; if your drain is longer than that window, work still gets cut off.
-
No drain window is a 100% guarantee. The process can be hard-killed, so the last line of defence has to live in the code itself.
What to do
-
Catch SIGTERM and stop accepting new requests. Fail your readiness probe or deregister from the load balancer so it stops routing new requests to you. Don’t close existing connections yet.
-
In Go, use
server.Shutdown(ctx). Catch SIGTERM withsignal.NotifyContextand handhttp.Servera context with a deadline. It refuses new connections and lets open requests finish until the timeout. -
In Octane, use a graceful stop/reload. Workers finish the current request and stop claiming new ones. On the queue side,
queue:workhandles SIGTERM itself as long as you don’tkill -9it. -
Align the platform’s grace period with your drain. Use an ASG lifecycle hook or k8s
terminationGracePeriodSecondsto match the platform’s wait to your longest acceptable drain time. -
Make long jobs idempotent/resumable. Every step idempotent, so an interrupted job can safely restart or resume.
Bottom line: order matters — drain the LB first, then stop new intake, then finish in-flight work, then die. In Go that’s server.Shutdown(ctx); in Octane it’s a graceful reload plus queue:work’s native SIGTERM behavior; on the platform side, align the grace period with your real drain time. Make jobs idempotent on top of that and even a hard kill won’t corrupt data.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.