nautechsystems/nautilus_trader · warning
Duplicate fill
Error message
Duplicate fill
What it means
The engine validates fills before applying them: if the order reports the fill's trade_id as a duplicate (order.is_duplicate_fill), the fill is skipped and this error is raised instead of re-applying state. Duplicate fills would otherwise double-count fills/positions.
Source
Thrown at crates/execution/src/engine/mod.rs:3486
if self.config.debug {
log::debug!("Generated {} for {}", position_id, fill.client_order_id());
}
position_id
}
fn determine_netting_position_id(&self, fill: &OrderFilled) -> PositionId {
PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
}
fn validate_fill_for_order(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
if order.is_duplicate_fill(fill) {
log::warn!(
"Duplicate fill: {} trade_id={} already applied, skipping",
order.client_order_id(),
fill.trade_id
);
anyhow::bail!("Duplicate fill");
}
if let Some(position_id) = fill.position_id
&& self.position_contains_trade_id(position_id, fill.trade_id)
{
log::warn!(
"Duplicate fill: {} trade_id={} already applied to position {}, skipping",
order.client_order_id(),
fill.trade_id,
position_id
);
anyhow::bail!("Duplicate position fill");
}
self.check_overfill(order, fill)
}
fn validate_fill_for_order_projection(View on GitHub (pinned to 18893faf8b)
Solutions
- This is a guard: treat it as 'already processed' and skip — deduplicate upstream by tracking processed trade_ids.
- If your code propagates the error, catch it and continue when the fill is a benign duplicate.
- Fix duplicate event delivery: check for double subscription or re-delivery after WebSocket reconnect.
- If reconciliation is the source, let it skip already-applied fills instead of re-submitting them to the fill path.
Example fix
// before
engine.apply_fill(&order, &fill)?; // bails on replayed trade_id
// after
match engine.apply_fill(&order, &fill) {
Err(e) if e.to_string().contains("Duplicate fill") => log::debug!("skipped duplicate {}", fill.trade_id),
other => other?,
} Defensive patterns
Strategy: try-catch
Validate before calling
// check before applying
if order.is_duplicate_fill(&fill) { return Ok(()); } Try / catch
match engine.apply_fill(order, &fill) {
Err(e) if e.to_string().contains("Duplicate fill") => log::debug!("fill {} already applied", fill.trade_id),
other => other?,
} Prevention
- Maintain a processed trade_id set on the caller side before forwarding fills to the engine.
- Deduplicate reconciliation fill reports against already-applied live fills.
- Guard against WebSocket replay/double delivery after reconnects.
When it happens
Trigger: Calling the engine's fill application path (e.g. update_order_filled / process_fill) with an OrderFilled event whose trade_id was already applied to the order — typically a replayed or duplicated venue event.
Common situations: Reconciliation re-delivering a fill that was already applied; venue/WebSocket message replay after reconnect; processing the same fill from both the execution report stream and reconciliation reports.
Related errors
- Duplicate position fill
- Failed to reserve execution intent for signer {} on chain {}
- Failed to commit replacement transaction: {e}
- Order for {} not found to determine position ID
- Binance Futures position request has unresolved instrument {
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/ac520cb230fbe11e.
Report an issue: GitHub.