The HTTP Problem: Request-Response Is Not Real-Time
Browsers talk to servers over HTTP. This protocol has been the backbone of the internet since 1991, but it was never designed for real-time applications.
HTTP rests on one simple principle: request-response.
- The browser asks: "Give me this data."
- The server answers: "Here is the data."
- The connection closes.
HTTP Request-Response Cycle: Connection ends after every response
Why is this problematic?
| Feature | In the past (1990s) | Today |
|---|---|---|
| Use Case | Reading static content | Editing content collaboratively |
| Interaction | Single user, no real-time updates | Multiple users editing simultaneously |
| Designed for | ✓ HTTP was developed for this | ✗ HTTP fundamentally fails here |
HTTP was built for the first case (static content for a single user). For the second case (collaborative real-time apps), it falls short at a fundamental level.
The core HTTP limitations:
- One-way communication: only the client can initiate requests, never the server.
- Request-response paradigm: every interaction has to be started by the client (even over Keep-Alive connections).
- No server push: the server cannot speak up to say, "Hey, there are updates over here!"
- Polling required: the browser has to keep asking whether anything has changed.
Since HTTP/1.1 (1997), connections stay open by default (Keep-Alive). This saves the time it takes to set up a connection, but it does not solve the underlying problem: the server still cannot initiate a message. It is still request-response, just over a connection that happens to already be open.
HTTP is pull (you ask for it). Real-time apps need push (the server tells you).
Table of Contents
An Everyday Example: The Shopping List
Situation: You and your partner both have a paper shopping list.
You look at your list
Your partner buys the milk
You ALSO buy milk
Result: you end up with twice as much milk, because your lists were never in sync.
Apps built on HTTP have exactly this problem.
The Same Problem with Apps
The shopping list problem with HTTP
The Three Main Problems
1. Stale Data
You cannot tell that your partner has already bought the milk. The two of you are working from different lists.
2. No Automatic Updates
The server has no way to say, "Heads up, the milk has been crossed off." You have to reload the app yourself to see any updates.
3. Concurrent Changes
What if you both cross off an item at the same instant? One change gets lost. This is known as a "race condition", like a race where only one runner can win.
How do developers solve this problem today?
Developers fall back on three tricks to bend HTTP into a real-time tool anyway. All three take a fair amount of work to build:
Technical Term: Polling
Technically: setInterval() + fetch() – the browser asks for updates at regular intervals.
How it works
Every few seconds, the browser fires off a request: "Anything new?"
Everyday example
Picture checking your phone every five seconds to see whether a new message has arrived.
Code:
// The browser asks for new data every 5 seconds
setInterval(async () => {
const response = await fetch('/api/project/status')
const data = await response.json()
updateUI(data)
}, 5000) // Every 5 secondsProblems:
- Wasteful: the browser keeps asking even when nothing has changed
- Laggy: you only see a change on the next poll (up to 5 seconds later)
- Heavy server load: 1,000 users means 1,000 requests every 5 seconds
The Core Problem
All three are workarounds, nothing more. Developers burn a lot of time papering over the weaknesses of HTTP instead of building new features for the app.
The Solution: Reactive Backends
The New Principle: Automatic Updates
Imagine a shared shopping list on your phone. Your partner crosses off "milk", and the change shows up on your phone straight away. You never have to ask; it just happens.
| Feature | HTTP (old system) | Reactive Backends (new system) |
|---|---|---|
| Data Updates | You have to keep asking: "Anything new?" | The server notifies you of changes automatically |
| Analogy | Checking the letterbox every 5 seconds | Push notification on your mobile phone |
| Shopping List | Bought milk twice | Automatically synchronised |
How It Works – Example: A Shared Shopping List
- You open the app and see the shared shopping list.
- The server makes a note: "This person is viewing the shopping list."
- Your partner crosses off "milk": the server pushes the change to you automatically.
- Your app updates the list on its own, and you see the milk crossed off straight away.
Convex: A Practical Example

Click loads YouTube (Privacy)
Convex is a backend system that handles reactive updates for you, automatically.
In short: Convex takes care of synchronisation, updates, and connection management, so you can focus on your app logic.
Convex rests on three key capabilities:
- Automatic Memory (Reactive Queries) – the server knows who needs which data
- All or Nothing (Transactional Mutations) – changes are guaranteed to land in full
- Always Connected (Real-time Subscriptions) – a persistent connection with no code on your part
Function 1: Automatic Memory (Reactive Queries)
The problem: the server needs to know who to notify when something changes.
The solution: Convex automatically keeps track of who is viewing which data.
Back to the shopping list: when you and your partner open the shared list, the server notes who is looking at it. So when someone crosses off "milk", only those two browsers get notified and updated, not everyone else using the app.
Code Example
// Convex Query: Automatic Tracking
export const getShoppingList = query({
args: { listId: v.id("shoppingLists") },
handler: async (ctx, { listId }) => {
// Convex automatically remembers:
// "This browser is looking at this shopping list"
const list = await ctx.db.get(listId)
return list
},
})
// In the browser: Automatic updates
const shoppingList = useQuery(api.lists.getShoppingList, { listId })
// When someone crosses off "milk", the display updates automatically!Efficient: only the browsers that have the shopping list open receive updates.
No code required: you do not have to write anything; Convex handles it for you.
Precise: when your partner crosses off "milk", only you and anyone else viewing this list are notified, not people looking at other lists.
Function 2: All or Nothing (Transactional Mutations)
The problem: what happens if something goes wrong halfway through a change?
The solution: with Convex, the rule is simple: either everything succeeds, or nothing does.
Everyday example:
Picture a money transfer:
- Take money out of account A
- Put money into account B
Where is the disaster? If step 1 succeeds but step 2 does not, the money simply vanishes.
An atomic transaction means:
- Either BOTH steps happen
- Or NEITHER of them does
- Never just one of the two.
No broken data: you never end up with half-finished changes in your database.
Safe: if something goes wrong, everything is rolled back automatically.
Automatic: you do not have to write anything; Convex handles it on its own.
Code Example
// Convex Mutation: All or Nothing
export const checkOffItem = mutation({
args: {
listId: v.id("shoppingLists"),
itemName: v.string(),
},
handler: async (ctx, { listId, itemName }) => {
// Everything together is an atomic transaction
const list = await ctx.db.get(listId)
if (!list) {
throw new Error("Shopping list not found")
}
// Find itemThe four guarantees (the ACID principle):
- Atomicity (indivisible): all or nothing
- Consistency: the data always stays valid
- Isolation: concurrent changes do not interfere with one another
- Durability: successful changes are stored permanently
Function 3: Always Connected (Real-time Subscriptions)
The problem: how can the server tell you the moment something changes?
The solution: Convex holds a persistent connection open (much like a phone call).
The clever part about Convex: you never have to deal with WebSockets yourself.
Convex sets the open connection up for you. You write your app as normal, and the updates simply arrive.
Code Example
// This is what you have to write (very simple!):
import { ConvexProvider, ConvexReactClient } from 'convex/react'
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)
function App() {
return (
<ConvexProvider client={convex}>
{/* All components here receive automatic updates */}
<ShoppingListApp />
</ConvexProvider>
)
}
// In your component: Query data normallyYou do NOT have to write any code for:
- Opening a WebSocket connection
- Monitoring the connection
- Reconnecting after a drop
- Receiving updates
- Updating the UI
Convex handles all of it automatically.
With plain WebSockets: you write hundreds of lines of code (opening connections, handling drops, processing updates, and so on).
With Convex: you write two lines (useQuery, and you're done). Convex handles the rest.
Time saved: days or weeks of work shrink to minutes.
Comparison: Old vs. New
Here are the differences side by side:
| Feature | Traditional HTTP | Reactive Backend (Convex) |
|---|---|---|
| Data Consistency | Manual | Automatic |
| State Management | Client-side | Backend-controlled |
| Real-time Updates | Polling/WebSocket | Native Reactivity |
| Cache Invalidation | Manual | Automatic |
| Transactional Security | Application Logic | Platform-guaranteed |
| Development Complexity | High | Low |
Self-Hosting and Data Sovereignty
Alongside the managed cloud infrastructure, Convex also offers self-hosting for companies with specific compliance, data protection, or governance requirements.
The open-source backend can run on your own infrastructure. Full control over where your data lives matters above all for regulated industries and GDPR-compliant architectures.
Feature scope: the open-source backend includes all the core features (Queries, Mutations, Actions, Realtime Subscriptions, Transactions).
| Feature | Convex Self-Hosted Open-Source | Convex Managed Cloud |
|---|---|---|
| Queries, Mutations, Actions, Transactions | Identical functionality | Identical functionality |
| Realtime Subscriptions | Identical functionality | Identical functionality |
| Convex Dashboard | Included (hosted & managed by user) | Included (hosted & managed by provider) |
| Streaming Import/Export | Manual via npx convex export/import | Managed connectors (Fivetran, Airbyte) |
| Log Aggregation & Streaming | Custom solution (e.g., Loki, ELK) | Integrated log streaming (Axiom, Datadog) |
| Exception Monitoring | Custom solution (e.g., self-hosted Sentry) | Integrated Sentry support |
| Health & Insights Dashboard | Custom solution (e.g., Grafana, Prometheus) | Integrated dashboard |
| Automated Backups | Custom scripts (e.g., convex-self-hosted-backups in S3) | Automated backups (daily/weekly) |
| Point-in-Time Restore | Manual process (from available backups) | Managed point-in-time restore |
| Disaster Recovery | Full user responsibility (Multi-region setup) | Managed by Convex |
| Horizontal Scaling | Default single-node (requires code modification) | Automatic, managed scaling |
| Official Support & SLA | Community only (Discord, GitHub Issues) | Email & Priority Support (with SLAs in Pro Plan) |
| Certifications | User responsibility (certify own infrastructure) | SOC 2, HIPAA, GDPR (verified platform) |
GDPR considerations for the Managed Cloud: Convex is a US company. Using the hosted cloud means a third-country transfer, which calls for a Data Processing Agreement (DPA) and an assessment of the Standard Contractual Clauses (Art. 46 GDPR). For many applications that is perfectly adequate; it only becomes a sticking point with highly sensitive data (health or financial records) or where authorities and regulated industries impose strict on-premise requirements.
Recommendation: for most web apps (B2B SaaS, content platforms, e-commerce), the Managed Cloud with a DPA is fully GDPR-compliant. Self-hosting mainly pays off for banking and healthcare, public authorities, and companies with an explicit on-premise policy.
Fully Managed Backend-as-a-Service
Convex runs the entire infrastructure for you: hosting, scaling, monitoring, updates, and disaster recovery.
Advantages:
No ops overhead
Automatic patches and security updates
Globally distributed edge infrastructure
Pay-per-use pricing model
Use Cases:
Rapid prototyping, MVPs, SaaS startups without a DevOps team, agile projects with a time-to-market focus.
The Transition: From Old to New
How complicated is the transition?
Good news: you do not have to change everything at once. You can move across step by step.
Let's look at an example: just how much simpler does the code get?
Before: With the Old System (HTTP)
What's wrong here?
Over 50 lines of code just to show a list
Requests every 5 seconds, even when nothing has changed
Fiddly: error handling, rollback and the rest all have to be coded by hand
Unsafe: it can still go wrong
Code Example
// Old system: Far too complicated!
// Backend (Express.js)
app.get('/api/projects/:id', async (req, res) => {
const project = await db.projects.findById(req.params.id)
res.json(project)
})
app.patch('/api/projects/:id', async (req, res) => {
const updated = await db.projects.update(req.params.id, req.body)
res.json(updated)
})
// Frontend (React + Redux)
const ProjectView = ({ projectId }) => {
const dispatch = useDispatch()After: With Convex (much simpler!)
The difference is dramatic.
70% less code (15 lines instead of 50)
No more constant requests (no polling)
Safe by default (atomic transactions)
Built-in error handling (nothing to code yourself)
Always in sync: every browser is guaranteed to see the same data
In short: instead of writing hundreds of lines of code, you reach for useQuery and useMutation, and Convex handles the rest.
Code Example
// Convex Backend (much simpler!)
export const getShoppingList = query({
args: { listId: v.id("shoppingLists") },
handler: async (ctx, { listId }) => {
// Simply fetch data - Convex automatically remembers
// who is looking at this list
return await ctx.db.get(listId)
},
})
export const checkOffItem = mutation({
args: {
listId: v.id("shoppingLists"),
itemName: v.string(),
},Speed Comparison (with Numbers!)
Imagine 1,000 people using your app at the same time.
What happens:
Every person asks every 5 seconds: "Anything new?"
That works out as:
-
12 requests per person per minute (60 seconds ÷ 5 = 12)
-
12,000 requests per minute across 1,000 people
-
Even when nothing changes.
Delay: 2 to 5 seconds before you see a change
Server load: very high (constant traffic)
99.9% fewer requests to the server
50x faster (from 5 seconds down to 0.1 seconds)
Far cheaper (lower server costs)
Limitations and Boundaries
Internet Connection Required
Reactive backends need a live connection to the server. Offline-first apps call for extra architecture (local caching, sync conflict handling).
Reactive systems are built first and foremost for online applications. Offline support means adding extra architectural layers (local replication, conflict resolution).
These apps need offline features:
Apps used offline a lot, apps in areas with poor connectivity, and mobile apps that have to work no matter what.
Migration Effort
Existing systems will need refactoring, so an incremental migration is the safer route.
Strangler Fig pattern: build new features on Convex, keep the legacy system running alongside, and migrate gradually.
Advantages: no big-bang risk, value delivered continuously, rollback possible at any point.
Offline support: hybrid setups with local state management (LocalStorage, IndexedDB) and sync-on-reconnect are possible, but take extra work to build.
Conclusion and Recommendation
YES: Real-time apps, collaborative tools, modern web apps
NO: Purely offline apps, simple static websites
Our take: for real-time applications, reactive backends are not optional. The question is not whether you will make the move, but when.
Further Links
- Convex Documentation – the official docs, with tutorials and examples. There is a free tier you can try.
- The Reactive Manifesto – the founding principles of reactive systems (a little technical, but well explained).
- RFC 6455: WebSocket Protocol – the official technical spec for WebSockets (for the deeply curious).