Python
async/await
concurrency
asynchronous programming
fire-and-forget

Fire and forget python async/await

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Python asyncio, "fire and forget" does not mean calling a coroutine and ignoring it. A coroutine object does nothing until it is awaited or scheduled. Real fire-and-forget behavior means creating a background task that the event loop can run independently.

The Correct Primitive: asyncio.create_task

If you are already inside an active event loop, the normal way to start a background coroutine is asyncio.create_task.

python
1import asyncio
2
3
4async def send_metric(name):
5    await asyncio.sleep(1)
6    print(f"sent {name}")
7
8
9async def main():
10    task = asyncio.create_task(send_metric("signup"))
11    await asyncio.sleep(2)
12
13
14asyncio.run(main())

The key point is that send_metric starts running because it was scheduled as a task. If you had written send_metric("signup") without awaiting or scheduling it, nothing would happen except a warning later.

Why Keeping A Reference Still Matters

Many developers hear "fire and forget" and interpret it as "never store the task". That is risky.

A background task can fail with an exception. If nothing observes the task, the failure may surface only as a noisy log message or be easy to miss.

A better pattern is to keep a reference long enough to attach error handling.

python
1import asyncio
2
3
4def log_task_result(task):
5    try:
6        task.result()
7    except Exception as exc:
8        print(f"background task failed: {exc}")
9
10
11async def background_work():
12    await asyncio.sleep(0.5)
13    raise RuntimeError("boom")
14
15
16async def main():
17    task = asyncio.create_task(background_work())
18    task.add_done_callback(log_task_result)
19    await asyncio.sleep(1)
20
21
22asyncio.run(main())

This is still "fire and forget" in the sense that the caller does not await the result directly, but the task is no longer invisible.

Fire And Forget Is Not A Process Boundary

Background tasks only live as long as the event loop lives. If your program exits immediately after scheduling a task, the task may never finish.

python
1import asyncio
2
3
4async def background():
5    await asyncio.sleep(1)
6    print("finished")
7
8
9async def main():
10    asyncio.create_task(background())
11
12
13asyncio.run(main())

This often prints nothing because main ends and the event loop shuts down before the background task completes.

So real fire-and-forget inside one process still requires the process to stay alive long enough for the task to run.

Good Use Cases

This pattern is reasonable for lightweight side work such as:

  • sending metrics
  • refreshing a cache
  • writing a non-critical audit record
  • notifying another internal service

It is a poor fit for work that must succeed even if the current request, process, or event loop ends. For durable background jobs, use a queue, worker process, or external job system instead.

Structured Alternative

Sometimes the best answer is not fire-and-forget at all. If the task matters, gather it explicitly or manage it inside a task group.

python
1import asyncio
2
3
4async def worker(name):
5    await asyncio.sleep(1)
6    return name
7
8
9async def main():
10    results = await asyncio.gather(worker("a"), worker("b"))
11    print(results)
12
13
14asyncio.run(main())

This is not fire-and-forget, but it is often safer because failures and completion are explicit.

Avoid Blocking Work In The Event Loop

If the background work is CPU-bound or blocking, create_task is not enough because the coroutine may still block the loop internally. In that case, run blocking work in an executor or a separate worker.

python
1import asyncio
2import time
3
4
5def blocking_job():
6    time.sleep(2)
7    print("blocking job done")
8
9
10async def main():
11    loop = asyncio.get_running_loop()
12    loop.run_in_executor(None, blocking_job)
13    await asyncio.sleep(3)
14
15
16asyncio.run(main())

This is often the right fire-and-forget pattern when the code you need to run is not asynchronous in the first place.

Common Pitfalls

The biggest mistake is creating a coroutine object and assuming it is already running. Coroutines must be awaited or scheduled.

Another issue is scheduling a task and then letting the program exit immediately. Fire-and-forget does not survive event-loop shutdown.

Developers also often ignore exceptions from background tasks. If the work matters at all, attach logging or some form of supervision.

Finally, do not use in-process fire-and-forget for durable business-critical work. If the task must outlive the request or the process, use a real job queue.

Summary

  • In Python asyncio, fire-and-forget means scheduling a coroutine with asyncio.create_task, not merely calling it.
  • Keep a reference or callback if you need to observe exceptions.
  • Background tasks stop when the event loop stops.
  • Use executors for blocking work that should run in the background.
  • For important or durable jobs, prefer an external worker or queue over in-process fire-and-forget.

Course illustration
Course illustration

All Rights Reserved.