stocky-oil-29870
02/23/2026, 9:44 AMtall-airline-16756
02/23/2026, 9:44 AMtall-airline-16756
02/23/2026, 9:45 AMwebhook_on_shutdown to run, but it’s not firing. I’ll explain why that happens and give practical ways to ensure your backend reliably gets notified when egress fails.
Why webhook_on_shutdown doesn’t run when you delete the room
• webhook_on_shutdown is a shutdown callback tied to the agent session (AgentSession / JobContext). It runs when the agent shuts down gracefully (e.g., via session.shutdown() / ctx.shutdown()).
• Deleting a room via the server API forces participants (including agents) to be disconnected immediately. That forced disconnection does not run the agent’s graceful shutdown path, so any shutdown callbacks are skipped.
• In short: room deletion is a room lifecycle event; shutdown hooks are part of the agent session lifecycle. Forcible room deletion bypasses agent shutdown hooks.
What you can do instead (recommended)
• Use LiveKit webhooks for reliable, backend-delivered notifications. LiveKit sends events like egress_ended (with status: EGRESS_FAILED) and room_finished even when the room is programmatically deleted.
◦ Configure your webhook URL and handle egress_ended in your backend.
◦ Webhooks are intended to notify your backend regardless of how the room ended, so you won’t rely on an agent to be able to run shutdown hooks.
Two practical approaches you can pick from
1) Recommended — Backend reacts to webhooks
• Let your backend receive and handle egress_ended (or room_finished) and perform whatever follow-up you need (notifications, logging, retries, cleanup).
• This works even if the room is deleted forcefully and the agent never gets a chance to run shutdown callbacks.
2) If you must run agent shutdown logic first — perform a graceful teardown
• Detect egress failure in the agent (or the service orchestrating the agent).
• Call the agent’s graceful shutdown API (session.shutdown() / ctx.shutdown() or equivalent) so shutdown callbacks (including webhook_on_shutdown) run.
• After the shutdown completes, have the backend delete the room. This ensures the agent hooks execute before the room is removed.
Which approach you should use
• If you need guaranteed backend notification regardless of how the room ends: use webhooks (approach 1).
• If you have critical agent-side cleanup that must run before room deletion: implement a graceful shutdown flow where the agent shuts down first, then the room is deleted (approach 2).
If helpful, I can:
• Show an example webhook handler for egress_ended with the key fields to check.
• Outline the minimal graceful shutdown sequence you’d call from the agent.
• Help you decide which approach fits your architecture better — do you run agents in an environment where they can always perform a graceful shutdown before a room is deleted? Sources: Job lifecycle | LiveKit Documentation | examples/survey | livekit/livekit_composite | DeepWiki | server-sdk-ruby | livekit/livekit_composite | DeepWiki | Best practices for managing webhook event streams | LiveKitstocky-oil-29870
02/23/2026, 10:15 AMtall-airline-16756
02/23/2026, 10:15 AMwebhook_on_shutdown callback runs before you delete the room. I’ll include Python and Node.js versions. Key points:
• Call the agent shutdown method with drain enabled so pending audio completes.
• Register your shutdown callback with the job context so it runs during shutdown.
• Delete the room only after shutdown completes (either inside the shutdown callback or after awaiting the shutdown).
Python (async) example
import asyncio
from livekit_agents import AgentSession, JobContext, inference, silero
import logging
logger = logging.getLogger(__name__)
async def entrypoint(ctx: JobContext):
# Create session with components
session = AgentSession(
stt=inference.STT(),
llm=inference.LLM(),
tts=inference.TTS(),
vad=silero.VAD.load(),
)
# Register shutdown callback on the job context
async def webhook_on_shutdown():
<http://logger.info|logger.info>("Running post-shutdown webhook...")
# perform webhook call to your backend, send logs, etc.
# Example (pseudo):
# await <http://http_client.post|http_client.post>(WEBHOOK_URL, json={"room": ctx.room, "reason": "shutdown"})
<http://logger.info|logger.info>("Post-shutdown webhook complete.")
# Optionally: delete the room from your server here (if permitted)
# await delete_room_api(ctx.room)
ctx.add_shutdown_callback(webhook_on_shutdown)
# Start the session (agent implementation omitted)
await session.start(agent=MyAgent(), room=ctx.room)
# ... normal agent work ...
# When you need to shut down gracefully:
# This requests a graceful shutdown and allows the session to drain (finish pending speech)
await session.shutdown(drain=True)
# Wait for session to fully close if shutdown returns quickly
await session.wait_closed()
# If you prefer to delete room from here rather than inside webhook:
# await delete_room_api(ctx.room)
Node.js / TypeScript example
import { defineAgent, JobContext } from "livekit-agents";
import { inference } from "livekit-agents/inference";
import { silero } from "livekit-agents/vad";
export default defineAgent({
entry: async (ctx: JobContext) => {
const session = new voice.AgentSession({
stt: new inference.STT(),
llm: new inference.LLM(),
tts: new inference.TTS(),
vad: await silero.VAD.load(),
});
// Register shutdown callback
ctx.addShutdownCallback(async () => {
console.log("Running post-shutdown webhook...");
// perform webhook call to your backend, send logs, etc.
// await fetch(WEBHOOK_URL, { method: "POST", body: JSON.stringify({ room: ctx.room }) });
console.log("Post-shutdown webhook complete.");
// Optionally: delete the room from your server here
// await deleteRoomApi(ctx.room);
});
await session.start({ agent: new MyAgent(), room: ctx.room });
// ... normal agent work ...
// Initiate graceful shutdown via the session API or job context
// Using job context:
await ctx.shutdown({ drain: true });
// Or using the session API if provided:
// await session.shutdown({ drain: true });
// Wait until fully closed if an awaitable exists
await session.waitClosed?.();
},
});
Notes and best practices
• Use drain=True (Python) or drain: true (Node) so pending TTS/playback finishes before teardown.
• Register shutdown callbacks on the JobContext so they run as part of the graceful shutdown sequence.
• If your orchestration sometimes forcefully deletes rooms, rely on backend webhooks (egress_ended, room_finished) as a fallback — but the graceful shutdown flow above ensures your agent-side webhook_on_shutdown runs when you trigger shutdown properly.
• Place any room-deletion API call after shutdown completes (inside the shutdown callback or immediately after await session.wait_closed()), to ensure ordering.
If you want, tell me:
• which language (Python or Node) and environment you’re using and
• whether you prefer deleting the room from the agent or from your backend,
and I’ll tailor the exact code for your setup (including an HTTP webhook POST snippet).
Sources: Job lifecycle | LiveKit Documentation