← Back to list

3 Rust Collection Patterns My Team Ignored -Until Latency Hit 900ms at 3 AM

At 3:07 AM, our API latency jumped from 42ms to 900ms.

DevLogic - Engineering Thinking · 2026-05-20 14:00 · 3 claps · 2.9 min read paywalled
#rust #backend-development #software-development #coding #programming
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development

3 Rust Collection Patterns My Team Ignored -Until Latency Hit 900ms at 3 AM

At 3:07 AM, our API latency jumped from 42ms to 900ms.

Nobody touched production.

Nobody deployed anything.

And yet dashboards looked like a heart attack.

Requests piled up. CPU usage spiked. Kafka lag started growing.

One engineer blamed PostgreSQL.

Another blamed Kubernetes networking.

I opened the profiler expecting some terrifying distributed systems problem.

Instead, the bottleneck was sitting inside a Rust Vec.

That moment was deeply humiliating.

Because the system was not collapsing from architecture.

It was collapsing from collection patterns we ignored for months.

Tiny mistakes. Repeated millions of times. Quietly burning CPU until production finally screamed loud enough for us to notice.

Those three lessons permanently changed how my team writes Rust.

1. Stop Using Vec::contains() Like It Is Free

This one hurt the most because the code looked perfectly innocent.

We had logic like this everywhere:

if users.contains(&id) {
    process(id);
}

Simple.

Readable.

Terrible at scale.

Why?

Because Vec::contains() performs a linear scan.

Every lookup walks through the collection one element at a time.

At small scale, nobody notices.

At production scale:

50 lookups  -> fine
5 million lookups -> disaster

Our hot path looked like this:

Request
   |
   v
Vec Scan
   |
   v
Another Vec Scan
   |
   v
Another Vec Scan

CPU usage exploded under load.

The fix was embarrassingly simple:

use std::collections::HashSet;

let users: HashSet<u64> = ids.into_iter().collect();

if users.contains(&id) {
    process(id);
}

Benchmark:

Before: 900ms
After: 74ms

That optimization alone made the service feel reborn.

And honestly, it taught me something important:

Readable code is not automatically cheap code.

2. We Kept Cloning Massive Collections Without Realizing It

Rust makes ownership explicit.

Which means cloning feels emotionally safe.

Too safe sometimes.

We had code like this everywhere:

let copy = items.clone();

Nobody questioned it because the compiler stayed happy.

Production did not stay happy.

One profiling session revealed we were cloning huge vectors inside request handlers thousands of times per second.

Memory allocations exploded.

Allocator pressure skyrocketed.

Latency followed immediately.

The painful part?

The code looked harmless during review.

The fix was changing how we passed data around:

fn handle(items: &[Item]) {
    for item in items {
        work(item);
    }
}

Borrow instead of clone.

That one mindset shift reduced allocation spikes massively.

Memory graph before:

^^^^^^^^^^^^^^^^^^^^

After:

____----____----___

Cleaner. Predictable. Stable.

Rust’s ownership model is not there to annoy you.

It is trying to save you from yourself.

3. We Built Temporary Collections Everywhere

This was the sneakiest issue of all.

We chained iterator operations beautifully:

let out = users
    .iter()
    .filter(|u| u.active)
    .map(|u| u.id)
    .collect::<Vec<_>>();

Looks elegant.

Feels modern.

But we kept doing unnecessary intermediate allocations in critical paths.

Especially inside loops.

We found patterns like this:

for batch in jobs {
    let ids = batch
        .iter()
        .map(|x| x.id)
        .collect::<Vec<_>>();

        send(ids);
}

Every loop iteration created fresh heap allocations.

At scale, allocator overhead became visible in latency graphs.

The fix was using reusable buffers:

let mut ids = Vec::with_capacity(1000);

for batch in jobs {
    ids.clear();
    for x in batch {
        ids.push(x.id);
    }
    send(&ids);
}

Benchmark results shocked the team:

Allocator CPU:
38% -> 9%

Same logic.

Same business behavior.

Wildly different runtime cost.

The Night Everything Finally Made Sense

That production incident changed how we review Rust code forever.

Before, we mostly reviewed for:

  • Correctness
  • Safety
  • Readability

Now we also review for:

  • Allocation behavior
  • Lookup complexity
  • Cache friendliness
  • Collection lifetime
  • Memory movement

Because Rust performance problems rarely arrive dramatically.

They accumulate quietly.

One clone here. One allocation there. One linear scan nobody notices.

Then traffic grows.

And suddenly your backend feels like it is dragging concrete blocks through mud.

What Surprised Me Most About Rust Performance

The biggest wins were never clever algorithms.

They were usually boring collection decisions.

Choosing HashSet over Vec. Borrowing over cloning. Reusing memory instead of reallocating.

Tiny engineering choices.

Massive production consequences.

That is what makes Rust fascinating to me now.

The language forces you to confront costs most ecosystems hide completely.

Memory has cost. Allocations have cost. Copies have cost. Data movement has cost.

Rust simply refuses to lie about it.

And honestly, after watching one innocent Vec::contains() push latency to 900ms in the middle of the night, I stopped treating collections like harmless containers.

They are architecture decisions disguised as data structures.


메타데이터
post_id
e4f474eb098d
slug
3-rust-collection-patterns-my-team-ignored-until-latency-hit-900ms-at-3-am-e4f474eb098d
url
https://medium.com/@devlogicwrites/3-rust-collection-patterns-my-team-ignored-until-latency-hit-900ms-at-3-am-e4f474eb098d
canonical_url
https://medium.com/@devlogicwrites/3-rust-collection-patterns-my-team-ignored-until-latency-hit-900ms-at-3-am-e4f474eb098d
author_url
https://medium.com/@devlogicwrites
status
ok
fetched_at
2026-06-09 15:37:30