PushFlo
Back to Blog
8 min readBy Marek

WebSocket Alternatives for Vercel and Cloudflare Workers

Vercel and Cloudflare Workers don't support WebSockets natively. Here are your options for adding real-time features to serverless apps, from polling to managed services.

vercelcloudflare-workerswebsocketserverlessreal-time

You chose serverless for good reasons: zero infrastructure management, automatic scaling, pay-per-use pricing, and instant global deployment. Vercel and Cloudflare Workers deliver on all of this.

Then you tried to add real-time features and discovered the catch: WebSockets don't work.

This isn't a bug — it's a fundamental limitation of the serverless model. But it doesn't mean you're stuck. In this guide, I'll walk through every option for adding real-time functionality to your Vercel or Cloudflare Workers app, with honest pros and cons for each.

Why WebSockets Don't Work on Serverless

First, let's understand the problem. WebSockets require:

  1. Persistent connections — A TCP connection that stays open indefinitely
  2. Stateful servers — The server must "remember" which clients are connected
  3. Bidirectional communication — Both client and server can send messages at any time

Serverless functions are the opposite:

  1. Request/response only — Function starts, processes request, returns response, terminates
  2. Stateless — No memory between invocations
  3. Timeout limits — Vercel functions max out at 60 seconds (Pro) or 300 seconds (Enterprise)

A WebSocket connection that needs to stay open for hours simply cannot exist in a function that terminates after seconds.

Option 1: Polling (The "Works But..." Option)

The simplest approach: have clients repeatedly ask "anything new?"

// Client-side polling
setInterval(async () => {
  const response = await fetch('/api/updates');
  const updates = await response.json();
  if (updates.length > 0) {
    handleUpdates(updates);
  }
}, 5000); // Check every 5 seconds

Pros:

  • Works everywhere, no special infrastructure
  • Simple to implement
  • No third-party dependencies

Cons:

  • High latency — 5-second polling means 2.5 seconds average delay
  • Wasteful — Most requests return nothing
  • Expensive at scale — 10,000 users × 12 requests/minute = 7.2 million requests/hour
  • Poor UX — Users notice the delay

Verdict: Acceptable for low-frequency updates (checking order status every minute), but not viable for chat, notifications, or live collaboration.

Option 2: Long Polling (Polling's Smarter Cousin)

Long polling keeps the request open until there's data to return:

// Client-side long polling
async function longPoll() {
  try {
    const response = await fetch('/api/updates?wait=true');
    const updates = await response.json();
    handleUpdates(updates);
  } catch (error) {
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  longPoll(); // Immediately start next request
}

Pros:

  • Near-instant delivery when updates occur
  • More efficient than regular polling
  • Still works with standard HTTP

Cons:

  • Timeout issues — Serverless functions timeout; you need to return before the limit
  • Connection overhead — Still creating new connections frequently
  • Complex state management — Server needs to track pending requests
  • Vercel limitation — Max 60-second hold means forced reconnection cycles

Verdict: Better than polling, but the timeout limits make it awkward on serverless platforms.

Option 3: Server-Sent Events (SSE)

SSE is a one-way streaming protocol. The server can push events to the client over a single HTTP connection:

// API route (Vercel Edge Function)
export const config = { runtime: 'edge' };

export default async function handler(req) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      // Send an event every second
      for (let i = 0; i < 30; i++) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`)
        );
        await new Promise(r => setTimeout(r, 1000));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

Pros:

  • Native browser support (EventSource API)
  • Works with Edge Functions (longer timeout than serverless)
  • Lower overhead than WebSockets
  • Automatic reconnection built into the protocol

Cons:

  • One-way only — Server to client; client-to-server still needs HTTP requests
  • Still has timeouts — Edge functions eventually timeout too
  • No state coordination — Multiple function instances don't share connected clients
  • Can't broadcast — Each connection is isolated

Verdict: Good for streaming responses (AI chat completions), but not a full WebSocket replacement. You can't easily broadcast "user X just updated the document" to all other users.

Option 4: Third-Party Pub/Sub Services

This is where managed real-time services come in. The architecture:

  1. Clients connect to the third-party service via WebSocket
  2. Your serverless functions publish messages via REST API
  3. The service broadcasts messages to connected clients

Comparing the Options

ServiceFree TierPaid StartingBest For
PushFlo500K msg/mo$19/moIndie devs, small teams
Pusher200K msg/day$49/moEstablished startups
Ably6M msg/mo$29/moEnterprise features
Firebase RTDBSpark plan$25/mo+Google ecosystem
Supabase Realtime500 connectionsUsage-basedPostgres users

For Vercel deployments specifically, see our dedicated WebSockets for Vercel guide. If you're using other serverless platforms, check out Serverless WebSockets.

PushFlo Example (Vercel)

// Client: Subscribe to updates
import { PushFloClient } from '@pushflodev/sdk';

const client = new PushFloClient({ publishKey: 'your-publish-key' });
client.connect();
client.subscribe('notifications', {
  onMessage: (message) => {
    showNotification(message);
  },
});

// Server: Publish updates (Vercel API Route)
export async function POST(request) {
  // Do your business logic...

  // Then notify clients
  await fetch('https://api.pushflo.dev/api/v1/publish', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.PUSHFLO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      channel: 'notifications',
      event: 'new',
      data: { message: 'Something happened!' }
    }),
  });

  return Response.json({ success: true });
}

Pros:

  • True real-time — Sub-50ms latency
  • No infrastructure — Someone else runs the WebSocket servers
  • Scales automatically — From 1 to millions of connections
  • Works with any serverless platform — Vercel, Cloudflare, Netlify, AWS Lambda

Cons:

  • Third-party dependency — Your real-time features depend on their uptime
  • Cost at scale — High-volume apps pay more (though still cheaper than self-hosting)

Verdict: The practical choice for most serverless apps. You get real WebSocket functionality without compromising your serverless architecture.

Option 5: Cloudflare Durable Objects (Cloudflare-Specific)

If you're all-in on Cloudflare, Durable Objects offer a unique solution. They're stateful, single-threaded "actors" that can hold WebSocket connections:

// Durable Object class
export class ChatRoom {
  constructor(state, env) {
    this.state = state;
    this.sessions = [];
  }

  async fetch(request) {
    const upgradeHeader = request.headers.get('Upgrade');
    if (upgradeHeader === 'websocket') {
      const pair = new WebSocketPair();
      this.sessions.push(pair[1]);
      pair[1].accept();
      pair[1].addEventListener('message', (msg) => {
        this.broadcast(msg.data);
      });
      return new Response(null, { status: 101, webSocket: pair[0] });
    }
    return new Response('Expected WebSocket', { status: 400 });
  }

  broadcast(message) {
    this.sessions.forEach(ws => ws.send(message));
  }
}

Pros:

  • True WebSockets — Native support within Cloudflare's edge network
  • Stateful — Objects persist and maintain state
  • Global distribution — Runs at the edge, close to users

Cons:

  • Cloudflare lock-in — Only works on Cloudflare Workers
  • Learning curve — Different programming model than traditional serverless
  • Complexity — You're managing WebSocket logic yourself
  • Pricing — Durable Objects have their own pricing tier

Verdict: Powerful if you're committed to Cloudflare and willing to invest in learning the model. Overkill for simple notification features.

Decision Framework: Which Option Should You Choose?

Use this flowchart:

Is sub-second latency critical?
├── No → Consider Polling or Long Polling
└── Yes ↓

Are you Cloudflare-only and okay with vendor lock-in?
├── Yes → Consider Durable Objects
└── No ↓

Do you need full control over WebSocket logic?
├── Yes → Hybrid architecture (self-hosted)
└── No ↓

Use a managed pub/sub service (PushFlo, Pusher, Ably)

For most developers building on Vercel or Cloudflare Workers, a managed pub/sub service is the sweet spot:

  • Real WebSocket functionality
  • Zero infrastructure to manage
  • Works with your existing serverless setup
  • Free tiers to get started

Quick Start with PushFlo

Here's the minimal code to add real-time to your Vercel or Cloudflare Workers app:

1. Client-side (any framework):

import { PushFloClient } from '@pushflodev/sdk';

const client = new PushFloClient({ publishKey: 'YOUR_PUBLISH_KEY' });
client.connect();
client.subscribe('my-channel', {
  onMessage: (data) => {
    console.log('Received:', data);
  },
});

2. Server-side (Vercel API Route):

await fetch('https://api.pushflo.dev/api/v1/publish', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PUSHFLO_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    channel: 'my-channel',
    data: { hello: 'world' }
  }),
});

3. Server-side (Cloudflare Worker):

export default {
  async fetch(request, env) {
    await fetch('https://api.pushflo.dev/api/v1/publish', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.PUSHFLO_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        channel: 'my-channel',
        data: { hello: 'world' }
      }),
    });
    return new Response('Published!');
  }
}

Conclusion

WebSockets and serverless are architecturally incompatible — but that doesn't mean your users have to suffer with slow, polling-based experiences.

The modern approach is to separate concerns:

  • Your serverless functions handle business logic
  • A managed service handles real-time connections

This gives you the best of both worlds: the simplicity of serverless AND the instant updates of WebSockets.


Ready to add real-time to your serverless app? Start with PushFlo's free tier — 500K messages/month, no credit card required.

Real-time without managing WebSocket servers

PushFlo holds the connections so your serverless functions don’t have to.