When you work with Laravel long enough, you’ll eventually face large datasets.
Not “a few thousand rows” large.
I mean hundreds of thousands, or even millions of records.
At that point, the way you loop through data becomes more than a coding style preference,
it becomes a performance decision.
I’ve seen systems slow down, jobs crash, and memory get eaten silently just because of one wrong choice:
👉 chunk() vs cursor()
Let’s talk about it: simply, practically, and honestly.
The Problem: Looping Like It’s 2015
Many of us started with something like this:
$users = User::all();
foreach ($users as $user) {
// process user
}
It works… until it doesn’t.
Because all() loads everything into memory.
On large tables, this is how you get:
- High memory usage
- Slow response times
- Unexpected crashes in production
Laravel gives us better tools but each one has trade-offs.
chunk() : Batch Processing With Control
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// process user
}
});
What chunk() really does
- Fetches records in batches
- Each batch is loaded fully into memory
- After the callback finishes, Laravel frees that batch and moves on
Why it’s useful
✅ Fewer database queries
✅ Easy to reason about
✅ You get batch context
That last point matters more than you think.
For example:
- Calculating totals per batch
- Sending grouped notifications
- Writing aggregated logs
- Updating related records together
The hidden cost
❌ Each chunk is still fully loaded into memory
If:
- Your rows are heavy
- You use a large chunk size
- You’re already under memory pressure
Then chunk() can still hurt.
cursor(): Streaming Data Like a Pro
foreach (User::cursor() as $user) {
// process user
}
What cursor() really does
- Uses a database cursor
- Fetches one record at a time
- Keeps memory usage extremely low
This is the closest thing to true streaming you’ll get with Eloquent.
Why it shines
✅ Almost constant memory usage
✅ Perfect for massive datasets
✅ Great for background jobs and long-running scripts
If your table has millions of rows, cursor() can be the difference between:
- A job finishing successfully
- Or crashing halfway through
The trade-offs
❌ More database queries
❌ No batch-level context
❌ Harder to optimize batch logic
You lose the ability to think in groups everything becomes row-by-row.
A Real-World Example
I once worked on a reporting feature that failed with this error:
“Report too big to be displayed”
The root cause?
- All records were being loaded
- Memory exploded before the response finished
Switching from all() → chunk() fixed some cases.
Switching from chunk() → cursor() fixed all cases.
The report became slower, yes.
But it stopped crashing, and that mattered more.
Rule of Thumb (That Actually Works)
I use this mental checklist:
Use chunk() when:
- You need batch logic
- You need to group, summarize, or aggregate
- Memory usage is acceptable
- You want fewer queries
Use cursor() when:
- You’re dealing with huge tables
- Memory is critical
- You’re running background jobs
- You don’t care about batch context
If you’re unsure:
👉 Start with chunk()
👉 Switch to cursor() when memory becomes a problem
Finally
Laravel gives us powerful abstractions, but they don’t remove responsibility.
Two lines of code can look almost identical
yet behave very differently under real production load.
Understanding tools like chunk() and cursor() is not about being clever.
It’s about being reliable.
And in production, reliability always wins.
Happy coding 👋