When I was building my Snake Game, I ran into a small but annoying problem. Every time someone refreshed the page, the leaderboard wiped clean. All those high scores, gone. I needed some way to save them without setting up a whole backend just for a browser game.
That is when I properly sat down with localStorage. I had used it casually before, but this time I actually needed to understand it. And what I found is that it is genuinely useful, but only if you know exactly where it stops being useful.
So what is localStorage, exactly?
It is a key-value store that lives inside your browser. No server. No database setup. No npm packages. It is just there, built into every modern browser, and it saves data that survives page refreshes, tab closes, even full browser restarts.
The entire API is five methods:
- localStorage.setItem('key', 'value') — saves something
- localStorage.getItem('key') — reads it back
- localStorage.removeItem('key') — deletes one entry
- localStorage.clear() — wipes everything
- localStorage.key(index) — gets a key by its position
That is genuinely the whole thing. Simple enough that you can learn it in ten minutes. The tricky part is understanding its rules, because it has a few that will silently break your app if you ignore them.
How I used it in the ToDo App
My ToDo App needed one thing: tasks should still be there when you come back. That is a perfect fit for localStorage because it is a single user, a single browser, and the data is simple, just a list of task objects.
The pattern I used is two steps. First, save on every change:
function saveTasks(tasks) {
localStorage.setItem('tasks', JSON.stringify(tasks));
}And second, load on page start:
function loadTasks() {
const saved = localStorage.getItem('tasks');
return saved ? JSON.parse(saved) : [];
}That is the core pattern. Every time you add a task, delete one, or tick it complete, call saveTasks. When the page loads, call loadTasks. Two functions. That is genuinely all you need for basic persistence.
Notice the JSON.stringify and JSON.parse. That brings me to the first rule you must never forget.
How I used it in the Snake Game leaderboard
The leaderboard was a bit more interesting because it is not just a flat list. It is sorted data with a cap on how many entries you keep. Every time a game ends, I needed to read the existing scores, add the new one, sort from highest to lowest, trim to the top 10, then save it back.
function saveScore(name, score) {
const existing = JSON.parse(localStorage.getItem('leaderboard') || '[]');
existing.push({ name, score });
existing.sort((a, b) => b.score - a.score);
const top10 = existing.slice(0, 10);
localStorage.setItem('leaderboard', JSON.stringify(top10));
}Read, modify, write back. That is the full database operation and it works perfectly for this use case. The leaderboard loads instantly, survives refreshes, and the payload stays small because we cap it at 10 entries.
This is localStorage behaving almost like a sorted table. You are just managing the sorting yourself instead of writing a SQL query.
The rules that will catch you off guard
Here is what nobody tells you clearly when you first start using localStorage:
- The storage limit is around 5 to 10 MB. It varies by browser. If you exceed it, the browser throws a QuotaExceededError and if you are not catching that error, your app breaks silently.
- It is synchronous. Every read and write blocks the main thread. For small data this is fine. For large datasets it becomes a real performance issue.
- It is scoped to the origin. Data saved on localhost:3000 is completely separate from localhost:5000. This trips up developers who switch dev server ports.
- It is not accessible in Web Workers or Service Workers. If you ever build offline functionality, you cannot use localStorage there.
Where it genuinely falls apart
There is a ceiling, and it is worth being honest about it. Once your use case grows past a certain point, localStorage stops being a solution and starts being a problem.
You cannot query it. There is no equivalent of WHERE score > 50 or ORDER BY date. To filter anything, you load the entire dataset into memory and process it in JavaScript. That is fine for 10 leaderboard entries. It is not fine for 10,000.
There is no schema. Nothing stops you from saving malformed JSON or using the wrong key name. One typo in your key string and you are reading null wondering why your data disappeared.
It does not sync across devices. A user who completes your ToDo App on their laptop will see nothing on their phone. For anything that needs to follow the user around, you need a real backend.
And in private or incognito mode, some browsers either block localStorage entirely or clear it the moment the tab closes. If your app depends on it for core functionality, that is a problem you have to handle.
So what should you use instead?
It depends entirely on what you actually need:
- localStorage — user preferences, UI state, simple single-user persistence. Exactly what the ToDo App and Snake leaderboard needed.
- sessionStorage — same API, but data clears when the tab closes. Good for temporary form state or single-session data.
- IndexedDB — a real browser database. Supports large datasets, binary data, and actual queries. Much more complex API, but the right tool when localStorage runs out of room.
- A real backend — MySQL, PostgreSQL, Firebase. The moment you need multiple users, authentication, or cross-device sync, you need a server. That is exactly why my Voting System and Student Portal use PHP and MySQL instead of anything client-side.
Final thoughts
localStorage is not a real database, and it does not try to be. But for the kind of small, self-contained persistence that browser projects often need, keeping a leaderboard alive, remembering a user's tasks, saving a theme preference, it is genuinely the right tool.
The Snake Game leaderboard works perfectly with it. The ToDo App works perfectly with it. If either of those needed user accounts or cross-device sync, I would reach for a backend immediately. But they do not and that is exactly the decision you need to make before you write a single line of storage code.
Know the boundary. Use it confidently within that boundary. And the moment your requirements outgrow it, move on without looking back.


