Graceful shutdown

Cloud, deploys and infrastructure Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

What does the program print?

Python 3.13
import asyncio


async def handle(i: int, done: list[int]) -> None:
    await asyncio.sleep(0.05)
    done.append(i)


async def main() -> None:
    done: list[int] = []
    accepting = True
    in_flight = {asyncio.create_task(handle(i, done)) for i in range(3)}
    # SIGTERM arrives: stop taking new work, finish what's in flight
    accepting = False
    await asyncio.wait_for(asyncio.gather(*in_flight), timeout=5)
    print(accepting, sorted(done))


asyncio.run(main())

Options
Hint 1 of 1

What does gather wait for?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A False [0, 1, 2]

Correct. Intake stops, and all three in-flight requests finish before exit.

On SIGTERM the server stops accepting, then waits for in-flight work with a deadline. The flag is cleared before the wait, and gather lets all three 50 ms handlers finish well inside the 5-second timeout.

Why not the others?
  • B True [0, 1, 2]. accepting is set to False before draining.
  • C False []. gather waits for the in-flight tasks.
  • D A TimeoutError. Each takes 0.05 s, well inside 5 s.

30-second answer

Stop accepting new work but finish what's in flight. Mark itself not ready so the load balancer stops sending traffic, keep serving requests already received, let workers finish or return their current message to the queue, close database pools and flush logs and metrics, then exit before the platform's grace period (30 seconds by default in Kubernetes) ends and it's killed.

Practise saying it · 30 s. No recording, just a timer

Graceful shutdown.

Say your answer out loud, then check which of these points you covered:

  • Fail readiness
  • Drain in-flight
  • Close pools, flush
  • Inside grace period
  • Signals reach PID 1

Deep dive

Kubernetes may still route a few requests briefly after SIGTERM; a short preStop delay covers it.

Long-running jobs need checkpoints or must be resumable.

Queue consumers should stop polling, finish the current message and commit offsets.

Make sure your process (PID 1 in the container) actually receives the signal.

Stop intake, then drain Ran on Python 3.13.5 · mypy --strict
Python 3.13
import asyncio


async def handle(i: int, done: list[int]) -> None:
    await asyncio.sleep(0.05)
    done.append(i)


async def main() -> None:
    done: list[int] = []
    accepting = True
    in_flight = {asyncio.create_task(handle(i, done)) for i in range(3)}
    # SIGTERM arrives: stop taking new work, finish what's in flight
    accepting = False
    await asyncio.wait_for(asyncio.gather(*in_flight), timeout=5)
    print(accepting, sorted(done))


asyncio.run(main())

Common mistakes

  • Exiting immediately on SIGTERM, dropping requests.
  • A shell wrapper as PID 1 that doesn't forward signals.
  • Work that takes longer than the grace period.

Senior take & follow-ups

Stop intake, drain, close resources, exit inside the grace period.

Follow-up questions

Think of your answer first, then open the hint.

  1. What's the default Kubernetes grace period?

    Show hint

    30 seconds, then SIGKILL.

  2. Why fail readiness first?

    Show hint

    So new traffic goes elsewhere while in-flight work completes.

Suggest a fix if something here is wrong or out of date.