How I Fixed a Bug That Made Every Chat Message Appear Twice

I’ve been working on a real-time messaging feature for FindCoffeeMate, the app I’m building to help developers connect over coffee and collaborate on ideas. As both the product owner and the engineer building it, I spend a lot of time thinking about the user experience before writing code. Messaging was one of those features that had to feel instant. If someone sends a message, it should appear immediately without making them wonder if anything happened.
The stack was pretty straightforward. React on the frontend, Node.js on the backend, PostgreSQL for storing messages, and Socket.io to push new messages in real time.
Everything looked good.Or at least I thought it did.
I opened two browser windows, logged in with different accounts, and started chatting with myself. The first message went through instantly.
Then I noticed something strange.Every message appeared twice.I refreshed the page again.Still twice.I sent another message.
Again.
Two identical messages.
My first thought was simple.
"I was convinced the database was the problem."
Maybe I was inserting the same record twice. That felt like the most obvious explanation, so I stopped looking at React completely and started digging into PostgreSQL.
I queried the messages table.
SELECT * FROM messages ORDER BY created_at DESC;
To my surprise, every message existed only once.
That immediately ruled out the database.
So if PostgreSQL wasn't duplicating anything, maybe the API was.
I added logs around the endpoint that saves messages.
console.log("Saving message:", message);
Sent another message.
One request.One insert.One response.Everything looked normal.
That sent me down the wrong path.Now I started wondering if Socket.io was emitting the same event twice.So I added even more logs.
socket.on("send-message", (data) => {
console.log("Received:", data);
io.to(roomId).emit("receive-message", data);
});
I watched the terminal.One event.One emit.Nothing suspicious.
At this point I was just adding logs everywhere.
Frontend.
Backend.
Database.
Socket events.
If there was a place where text could possibly pass through, there was probably a console.log() waiting for it.
Then I opened the browser DevTools.The Network tab looked clean.
One API request. The WebSocket connection was alive. No duplicate HTTP requests. No unexpected reconnects.I switched over to the Console tab instead.
That's when the logs finally showed something interesting.
Every time a message arrived, my event handler was running twice. Not sometimes.Every single time.
That completely changed what I was looking for.
The server wasn't sending duplicate messages.
The client was listening twice.
I went back to my React component.
This was the code.
useEffect(() => {
socket.on("receive-message", (message) => {
setMessages((prev) => [...prev, message]);
});
}, [messages]);
At first glance, nothing looked terribly wrong.
Then I noticed the dependency array.
Every time messages changed, the effect ran again.
Which meant another event listener was registered.And another.And another.The old listeners never disappeared.
So when the server emitted one message, multiple listeners handled the exact same event.
That explained everything.
The fix ended up being much smaller than the time I spent finding it.
useEffect(() => {
const handleMessage = (message) => {
setMessages((prev) => [...prev, message]);
};
socket.on("receive-message", handleMessage);
return () => {
socket.off("receive-message", handleMessage);
};
}, []);
I refreshed the page again.
Sent one message.One message appeared.
Sent another.
Still one.
Opened another browser.
Everything stayed perfectly in sync.
No duplicates.
No mystery.
Just one message doing exactly what it was supposed to do.
The funny part is that I probably spent more time looking at PostgreSQL than I spent fixing the actual bug.
Because duplicate messages felt like a database issue.
Then they felt like a backend issue.
Then they felt like Socket.io reconnecting.
None of those were true.
It was just React happily registering event listeners over and over because I told it to.
Working on a product by yourself is interesting because you're constantly switching hats. One minute you're thinking like a product engineer, trying to make conversations feel natural and responsive. The next minute you're acting like the product owner, wondering why a user would lose confidence if every message suddenly appeared twice.
That mindset actually kept me debugging longer than I expected. I wasn't trying to make the code "correct." I was trying to make the product feel trustworthy.
One small bug can completely change how users perceive a feature.
This wasn't some complicated distributed systems problem.
It wasn't PostgreSQL.
It wasn't Node.js.
It wasn't Socket.io.
It was one React effect quietly registering multiple listeners while I kept looking everywhere else.
I still laugh a little thinking about how many logs I added before opening the right file.
Sometimes debugging isn't about finding a complicated bug.
Sometimes it's just about finally asking the right question.

