Strengthening Subscriptions with Retry Mechanisms in React · Project
While building a fullstack course builder app powered by AI, I came across an interesting challenge that many real-time apps might face - unreliable subscriptions. In this post, I’ll share how I built a robust retry mechanism for GraphQL subscriptions using URQL in a Next.js frontend.
The solution is generic, reusable, and might save you hours if you’re trying to stream data reliably to your users.
The Context: A Generative AI Course Builder
An app I’m working on at Lyearn helps users create courses using AI. Users input some setup information, and the system uses AI to generate everything - title, description, outcomes, a structure of the course, lessons, and even assessments. There's a human in the loop during the content generation process to ensure quality and relevance.

When generating multiple lessons, we stream lesson content to the user using GraphQL subscriptions. This makes the experience fast and smooth. Users start seeing content as it's generated rather than waiting for everything to finish.
But... there was a catch.
The Problem: Subscriptions That Silently Die
Sometimes, the GraphQL subscription would just stop receiving data
Maybe the WebSocket connection failed
Maybe the backend silently stopped sending updates
Or maybe the frontend never got informed about a backend task that had already finished
This is a big problem - especially when you’re building user-facing features that depend on real-time updates.
I started wondering: How reliable is a single subscription? And if it’s not, how do I make my app tough enough to handle these hiccups without leaving users hanging? That’s when I decided to build a retry mechanism—something reusable across all the AI-driven subscriptions in my frontend.

My Approach: A Four-Phase Safety Net
I came up with a step-by-step plan to catch failures and recover gracefully. Here’s how it works in simple terms:
Phase 1: Start the Subscription
I kick off a normal subscription and set a 30-second timer. Every time new data streams in, I reset the timer. If 30 seconds pass with no data, something’s up, and I move to the next step.
Phase 2: Check with a Query
I ask the backend, “Hey, what’s the status of this task?” In my setup, I can check the task’s progress with a query too, not just subscriptions, which is super convenient. If the task is done, I update the screen (or execute the provided callback) and call it a day. If it’s still going, I look at the last update time. If it’s older than 30 seconds, I assume the backend’s stuck, throw an error, and stop. But if it’s recent, the problem is likely on my end - maybe a dropped connection - so I try again.
Phase 3: Retry the Subscription
I restart the subscription, set the 30-second timer again, and hope for the best. It’s like giving it a second chance to work properly.
Phase 4: Final Check
If the retry fails too (no data in 30 seconds), I make one last query. If the task still isn’t done, I call it a timeout and let the user know something’s wrong.
Why This Matters
Building this retry logic adds robustness to my app. In simple words, this means it can handle errors without crashing or confusing the user. Plus, once I got this working, I could use it anywhere in my app where subscriptions might flake out. It’s a one-time effort with big payoffs.
The Code: Two Versions of the Solution
I wrote two versions of this retry mechanism as React hooks, each helpful in different use cases. I’ve tried to keep the code generic so it can fit anyone’s needs and is easy to understand. Let’s break them down.
Version 1: The Generic Hook
This one runs the retry logic automatically when you use it. It manages subscriptions with retry logic, timeouts, and state updates. It’s perfect for most cases where you just want the data to flow in without any extra trouble.
Here’s the code:
This is how you use it:
The hook tracks the task’s data, whether it’s loading, any errors, and the current status (like “subscribing” or “timed_out”). Inside, useEffect
starts the subscription when there’s a task ID, and the executeSubscription
and executeQuery
functions handle the retry logic I described. The 30-second timer is managed with setTimeout
, and I useuseRef
to keep track of things like the active subscription so I can clean up properly.
Version 2: The Imperative Hook
Sometimes, React’s rules get in the way. You can’t call hooks conditionally, which can be a pain. So, I made an imperative version that gives you a function to call whenever you want.
Here’s that code:
Using it looks like this:
This version lets you control when the subscription starts by calling subscribe
yourself. You pass in handlers to react to data, errors, status changes, or completion. It’s flexible and gets around React’s Rules of Hooks.
Wrapping Up
This retry mechanism has been a game-changer for my course builder. Lessons load reliably, and users don’t get stuck staring at a blank screen. What started as a quick fix ended up being a reusable pattern that I can now plug into any real-time feature powered by subscriptions.

If your app deals with generative content, streaming updates, or unreliable WebSocket connections, this approach might be exactly what you need.