The HTTP Problem: Request-Response is not Real-Time
Browsers communicate with servers via HTTP. This protocol has been the foundation of the internet since 1991 – but it was never designed for real-time applications.
HTTP is based on a simple principle: Request-Response.
- Browser asks: "Give me this data"
- Server answers: "Here is the data"
- Connection is closed
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 developed for the first case (static content for single users). For the second case (collaborative real-time apps), it fundamentally fails.
The fundamental HTTP limitations:
- One-way communication: Only the client can initiate requests, never the server.
- Request-Response paradigm: Every interaction must be started by the client (even with Keep-Alive connections).
- No server push notifications: The server cannot actively say "Hey, there are updates here!"
- Polling necessary: The browser has to constantly ask if there are changes.
Since HTTP/1.1 (1997), connections remain open by default (Keep-Alive). This saves time when establishing a connection, but does not solve the fundamental problem: The server still cannot initiate messages. It is still request-response – just over an existing connection.
HTTP is pull (you ask for it). Real-time apps need push (the server informs 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 now have double the milk because your lists were not synchronised.
Apps using HTTP have exactly this problem!
The Same Problem with Apps
The shopping list problem with HTTP
The Three Main Problems
1. Stale Data
You do not see that your partner has already bought the milk. Both of you are working with different lists.
2. No Automatic Updates
The server cannot say: "Attention, the milk has been crossed off!" You have to reload the app yourself to see updates.
3. Concurrent Changes
What happens if both cross off an item at the same time? One change is lost. This is called a "race condition" – like a race where only one can win.
How do developers solve this problem today?
Developers use three tricks to use HTTP for real-time apps anyway. All three require significant implementation effort:
Technical Term: Polling
Technical: setInterval() + fetch() – The browser asks for updates at regular intervals.
How it works
The browser automatically sends a request every few seconds: "Is there any news?"
Everyday example
Imagine checking your smartphone every 5 seconds to see if 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:
- Waste: The browser asks even when nothing has changed
- Delay: You only see changes the next time you ask (up to 5 seconds later)
- Server load: 1000 users = 1000 requests every 5 seconds
The Core Problem
All three solutions are merely workarounds. Developers spend a lot of time compensating for the weaknesses of HTTP instead of developing new features for the app.
The Solution: Reactive Backends
The New Principle: Automatic Updates
Imagine you have a shared shopping list on your smartphone. Your partner crosses out "milk" – and immediately you see the change on your phone. You don't have to ask, it happens automatically!
| Feature | HTTP (old system) | Reactive Backends (new system) |
|---|---|---|
| Data Updates | You have to constantly ask: "Is there anything new?" | The server informs you automatically of changes |
| Analogy | Checking the letterbox every 5 seconds | Push notification on your mobile phone |
| Shopping List | Bought milk twice | Automatically synchronised |
How it works – Example: Shared Shopping List
- You open the app and see the shared shopping list
- The server remembers: "This person is looking at the shopping list"
- Your partner crosses off "milk": The server automatically sends you the change
- Your app updates the list by itself – you immediately see the crossed-out milk
Convex: A Practical Example
Klick lädt YouTube (Datenschutz)
Convex is a backend system that performs reactive updates automatically.
In short: Convex handles synchronisation, updates, and connection management. You focus on your app logic.
Convex has three important functions:
- Automatic Memory (Reactive Queries) – the server knows who needs which data
- All or Nothing (Transactional Mutations) – changes are guaranteed to be complete
- Always Connected (Real-time Subscriptions) – persistent connection without programming effort
Function 1: Automatic Memory (Reactive Queries)
The Problem: The server needs to know who to notify when something changes.
The Solution: Convex automatically remembers who is looking at which data.
Back to the shopping list: When you and your partner open the shared list, the server automatically remembers who is looking at it – and when someone crosses off "milk", only these two browsers are informed and updated, not all other users of 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 don't have to program anything - Convex does it automatically.
Precise: When your partner crosses off "milk", only you and other people with this list are informed – not people with other lists.
Function 2: All or Nothing (Transactional Mutations)
The Problem: What happens if something goes wrong in the middle of a change?
The Solution: With Convex, the rule is: Either everything works, or nothing at all.
Everyday example:
Imagine a money transfer:
- Debit money from account A
- Deposit money into account B
What would be disastrous? If step 1 succeeds, but step 2 does not. Then the money is gone!
Atomic transaction means:
- Either BOTH steps occur
- Or NEITHER of the two steps
- Never just one!
No broken data: You never have "half" changes in your database.
Secure: If something goes wrong, everything is automatically rolled back.
Automatic: You don't have to program 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 (ACID principle):
- Atomicity (Indivisible): All or nothing
- Consistency: Data always remains valid
- Isolation: Concurrent changes do not interfere with each other
- Durability: Successful changes are permanently stored
Function 3: Always Connected (Real-time Subscriptions)
The Problem: How can the server inform you immediately when something changes?
The Solution: Convex keeps a persistent connection open (like a phone call).
The brilliant thing about Convex: You don't have to worry about WebSockets!
Convex sets up the open connection automatically. You program your app normally, and updates arrive automatically.
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 program anything for:
- Establishing a WebSocket connection
- Monitoring the connection
- Reconnecting upon disconnection
- Receiving updates
- Updating the UI
Convex handles everything automatically!
With standard WebSockets: You have to write hundreds of lines of code (establishing connections, handling disconnections, processing updates, etc.)
With Convex: You write 2 lines of code (useQuery and you're done). Convex handles the rest automatically.
Time saved: Days or weeks of work turn into minutes!
Comparison: Old vs. New
Let's look at the differences in a table:
| 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
In addition to the managed cloud infrastructure, Convex also offers self-hosting options for companies with specific compliance, data protection, or governance requirements.
The open-source backend can be operated on your own infrastructure. Full control over the data infrastructure is crucial, especially for regulated industries and GDPR-compliant architectures.
Feature Scope: The open-source backend contains all 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 Managed Cloud: Convex is a US company. If you use the hosted cloud solution, this involves a third-country transfer, which requires a Data Processing Agreement (DPA) and an assessment of Standard Contractual Clauses (Art. 46 GDPR). For many applications, this is entirely sufficient – it only becomes critical for highly sensitive data (health data, financial data) or if authorities/regulated industries have 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 is primarily worthwhile for: banking/healthcare, government authorities, companies with an explicit on-premise policy.
Fully Managed Backend-as-a-Service
Convex takes over the entire infrastructure: 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 don't have to change everything at once. You can transition step by step.
Let's look at an example: How much simpler does the code become?
Before: With the Old System (HTTP)
What is the problem here?
Over 50 lines of code just for a simple display
Constant requests every 5 seconds (even when nothing changes!)
Complicated: Error handling, rollback, etc., must all be programmed manually
Insecure: Can still lead to problems
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 huge!
70% less code (15 lines instead of 50!)
No more constant requests (no polling)
Automatically secure (atomic transactions)
Built-in error handling (you don't have to program anything)
Always synchronised: All browsers are guaranteed to see the same data
In short: Instead of writing hundreds of lines of code, you use useQuery and useMutation. 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: "Is there anything new?"
That means:
-
12 requests per person per minute (60 seconds ÷ 5 = 12)
-
12,000 requests per minute with 1,000 people
-
Even if nothing changes!
Delay: 2-5 seconds until you see changes
Server load: Very high (constant traffic)
99.9% fewer requests to the server!
50x faster (from 5 seconds to 0.1 seconds)
Much cheaper (lower server costs)
Limitations and Boundaries
Internet Connection Required
Reactive backends require an active connection to the server. Offline-first apps require additional architecture (local caching, sync conflicts).
Reactive systems are primarily designed for online applications. Offline functionality requires additional architectural layers (local replication, conflict resolution).
These apps need offline features:
Apps that are frequently used offline, apps in areas with poor internet, and mobile apps that must always function.
Migration Effort
Existing systems require refactoring. An incremental migration is recommended.
Strangler Fig Pattern: New features with Convex, run legacy system in parallel, gradual migration.
Advantages: No big-bang risk, continuous value creation, rollback possible at any time.
Offline Support: Hybrid solutions with local state management (LocalStorage, IndexedDB) and sync-on-reconnect are possible, but require additional implementation.
Conclusion and Recommendation
YES: Real-time apps, collaborative tools, modern web apps
NO: Purely offline apps, simple static websites
Our assessment: For real-time applications, reactive backends are necessary. The question is not "if", but "when" you will make the transition.
Further Links
- Convex Documentation – Official documentation with tutorials and examples. There is a free version available for testing.
- The Reactive Manifesto – Basic principles of reactive systems (a bit technical, but well explained).
- RFC 6455: WebSocket Protocol – The official technical specification for WebSockets (for experts).