cloudflare/quiche · error
tokio task ID already in use
Error message
tokio task ID already in use: {id} What it means
task-killswitch's add_task_if panics when a tokio task::Id is inserted into the registry while a live (non-tombstone) entry with that same ID already exists. Task IDs should be unique per spawned task, so this indicates a bookkeeping bug: the same ID was added twice without being removed, i.e. an internal invariant violation of the registry.
Solutions
- Always pair every add_task_if with a remove_task (e.g. via the provided JoinHandle wrappers) so IDs are freed before reuse
- Check the map before inserting; treat an existing live entry as a bug and log/replace it deliberately
- Update task-killswitch / tokio to compatible versions if ID reuse behavior changed
- If the duplicate is intentional (task respawned), remove the stale entry first
Example fix
// before registry.add_task_if(id, handle, pred)?; registry.add_task_if(id, new_handle, pred)?; // panics: id still live // after registry.add_task_if(id, handle, pred)?; registry.remove_task(id); registry.add_task_if(id, new_handle, pred)?;
Defensive patterns
Strategy: try-catch
Validate before calling
// Rust: check before registering
if registry.contains_live_task(&id) {
registry.remove_task(id);
}
registry.add_task_if(id, handle, pred)?; Type guard
fn is_registered(map: &HashMap<task::Id, TaskEntry>, id: &task::Id) -> bool {
matches!(map.get(id), Some(TaskEntry::Live(_)))
} Try / catch
// task-killswitch panics on duplicate ID; there is no catchable error.
// Guard the call site and treat a panic as a bug:
let result = std::panic::catch_unwind(|| registry.add_task_if(id, handle, pred));
if result.is_err() {
// duplicate task id: log and recover
} Prevention
- Always pair add_task_if with remove_task via the provided handle wrappers
- Never re-register a task ID while its live entry exists
- Write tests that spawn/kill/re-spawn tasks through the killswitch API
- Keep task-killswitch and tokio versions in sync
When it happens
Trigger: Calling add_task_if (via the killswitch tracking wrapper) with a task::Id that already maps to a live TaskEntry; re-registering a task handle without a prior remove_task; racing registration logic that reuses IDs.
Common situations: Custom task-management code layered on the killswitch that spawns and re-registers handles manually, tests that add the same mocked task ID twice, upstream tokio behavior changes causing ID reuse.
Related errors
- The provided buffer is too large
- Can't use RPK when compiled without rpk feature
- --connect-to is expected to be a string containing an IPv4…
- initial send failed
- datagram extension not enabled by peer
AI-assisted analysis of cloudflare/quiche@9f96daa2c2 (2026-09-08).
Data as JSON: /api/errors/11d0bc7ac3b190da.
Report an issue: GitHub.
Appendix: source
Thrown at task-killswitch/src/lib.rs:194
fn add_task_if(
&self, handle: AbortHandle, cond: impl FnOnce() -> bool,
) -> Result<(), AbortHandle> {
use dashmap::Entry::*;
let id = handle.id();
match self.tasks.entry(id) {
Vacant(e) => {
if !cond() {
return Err(handle);
}
e.insert(TaskEntry::Handle(handle));
},
Occupied(e) if matches!(e.get(), TaskEntry::Tombstone) => {
// Task was removed before it was added. Clear the map entry and
// drop the handle.
e.remove();
},
Occupied(_) => panic!("tokio task ID already in use: {id}"),
}
Ok(())
}
fn remove_task(&self, id: task::Id) {
use dashmap::Entry::*;
match self.tasks.entry(id) {
Vacant(e) => {
// Task was not added yet, set a tombstone instead.
e.insert(TaskEntry::Tombstone);
},
Occupied(e) if matches!(e.get(), TaskEntry::Tombstone) => {},
Occupied(e) => {
e.remove();
},
}
}View on GitHub (pinned to 9f96daa2c2)