How to Use drain and retain on Collections in Rust

Use drain to remove and iterate over items, and retain to filter a collection in place based on a condition.

The problem with clearing and filtering

You have a vector of pending network requests. You want to send them all, wait for responses, and then clear the vector so it is ready for the next batch. Or you have a cache of user sessions and need to evict everything older than an hour without allocating a brand new vector. Rust gives you two methods for exactly these jobs: drain and retain. They modify collections in place, but they do it in fundamentally different ways.

The bucket stays. The water moves.

How drain actually moves data

drain takes a range of indices and removes those elements from the collection. It returns an iterator that yields owned values. The collection itself survives the operation, and it keeps its allocated capacity. This capacity preservation is the whole point. Allocating memory is expensive. Reusing the same heap block across multiple batches keeps your allocation counter flat and your cache lines warm.

fn process_task_batch(queue: &mut Vec<String>) {
    // drain(..) consumes the entire range and returns a Drain iterator.
    // The vector becomes empty but retains its heap allocation.
    let batch = queue.drain(..).collect::<Vec<_>>();

    // We now own the strings. We can move them into threads or functions.
    for task in batch {
        println!("Dispatching: {}", task);
    }
}

Under the hood, drain does three things. It marks the range as uninitialized. It shifts any remaining elements down to fill the gap. It returns a Drain iterator that holds a mutable reference to the vector and tracks how many items are left to yield. When you call next() on that iterator, it moves the next value out of the vector and drops the original slot. When the iterator finishes, the vector's length is updated to reflect the new, smaller size.

The shift operation is where performance lives. Draining from the end of a vector is cheap. Draining from the middle forces Rust to copy every element after the range down by the size of the removed slice. That is an O(N) operation relative to the tail. If you are draining the first hundred items out of a million, you just moved 999,900 elements in memory. The compiler will not stop you, but your profiler will notice.

Capacity survives. Elements shift. Memory stays hot.

How retain compacts in place

retain works differently. You give it a closure that returns a boolean. The method iterates through the collection once, keeps elements where the closure returns true, and drops everything else. You do not get the removed items. You just get a smaller collection with the same capacity.

fn evict_expired_sessions(cache: &mut Vec<Session>, now: u64) {
    // retain keeps items where the closure returns true.
    // It compacts the vector in a single pass without reallocating.
    cache.retain(|session| now - session.created_at < 3600);
}

struct Session {
    id: u32,
    created_at: u64,
}

The implementation uses a write index. It walks forward with a read index. When it finds an element that should stay, it copies it to the write index position and increments the write index. When it finds an element that should go, it drops it and leaves the write index behind. After the loop finishes, it drops any remaining elements between the write index and the old length, then updates the length to match the write index. This is a single O(N) pass with zero extra allocations. Order is preserved. Indices change.

The closure signature trips up beginners. retain passes &mut T to the closure, not &T. If you only need to read the value, you must destructure the mutable reference or ignore the mutability. Writing |x| x.is_valid() will fail to compile because x is &mut Session, not Session. The compiler rejects this with E0308 (mismatched types) if your function expects an owned value, or it forces you to handle the reference correctly.

Check your closure signature before the compiler checks it for you.

When the borrow checker fights back

Both methods require &mut self. That requirement creates a hard boundary with Rust's borrowing rules. You cannot hold a reference to the vector while calling drain or retain. The borrow checker enforces exclusive access because the methods mutate the collection's length and memory layout.

If you try to iterate over a reference and remove items simultaneously, you get E0502 (cannot borrow as mutable because it is also borrowed as immutable). The compiler sees the overlap and stops you. The fix is almost always to collect the indices or values you want to remove first, then call the removal method in a second pass.

let mut data = vec![10, 20, 30, 40];

// Collect indices first to avoid borrowing conflicts.
let to_remove: Vec<usize> = data.iter().enumerate()
    .filter(|(_, &val)| val > 25)
    .map(|(idx, _)| idx)
    .collect();

// Remove in reverse order to keep earlier indices valid.
for idx in to_remove.into_iter().rev() {
    data.remove(idx);
}

Notice the reverse iteration. Removing from the front shifts every subsequent element and invalidates higher indices. Removing from the back leaves lower indices untouched. This pattern is standard when you cannot use retain because you need the removed values, but you also cannot use drain because the removal condition depends on runtime state rather than a fixed range.

The borrow checker is not being difficult. It is preventing you from reading memory that is about to be shifted or dropped. Trust the error message. It points exactly to the overlapping lifetimes.

The modern alternative: drain_filter

Rust 1.53 stabilized drain_filter. It combines the behaviors of drain and retain. You get a closure that decides what to keep, and you get an iterator that yields the removed items. This closes the gap when you need to both filter in place and process the discarded values.

fn separate_active_and_idle(workers: &mut Vec<Worker>) {
    // drain_filter yields removed items while keeping the rest in place.
    // The closure receives &mut T and returns true to remove it.
    let idle: Vec<Worker> = workers.drain_filter(|w| w.is_idle()).collect();

    for worker in idle {
        println!("Terminating idle worker: {}", worker.id);
    }
}

struct Worker {
    id: u32,
    active: bool,
}

impl Worker {
    fn is_idle(&self) -> bool {
        !self.active
    }
}

drain_filter uses the same write-index compaction strategy as retain. The difference is that it stores the removed elements in a temporary buffer inside the iterator, then yields them by value. This means you get ownership of the filtered-out items without allocating a second vector upfront. It is the right tool when you need to hand removed items to a cleanup routine, a logging system, or a background thread.

Convention aside: the community prefers drain_filter over retain plus a separate scan whenever the removed items have side effects. Dropping a MutexGuard or flushing a database connection inside a removed item should happen deterministically. drain_filter guarantees that drop order matches iteration order.

Pick the tool that matches your ownership needs, not just your filtering logic.

Choosing the right removal strategy

Use drain when you need to take ownership of a contiguous range of items and process them outside the collection. Use drain(..) to empty a queue while preserving allocated capacity for the next batch. Use retain when you want to filter a collection in place and do not care about the removed values. Use drain_filter when you need to filter in place but also require ownership of the discarded items. Use into_iter when you are done with the collection entirely and can consume it by value. Use filter when you need a lazy iterator that produces a new collection without mutating the original. Use remove when you only need to extract a single element by index and do not want to shift the entire tail. Use clear when you want to drop every element immediately and do not need an iterator over the removed values.

The decision always comes down to three questions. Do you need the removed items? Do you need to keep the collection alive? Do you know the exact indices ahead of time? Answer those, and the method chooses itself.

Where to go next