Deploying a real-time 2P Co-op browser game with Vercel + Ably

Deploying a real-time 2P Co-op browser game with Vercel + Ably

Vercel (Next.js) and Ably (real-time Pub/Sub) were combined to deploy a two-player cooperative browser game without server management. I will explain the overall architecture and implementation techniques.
2026.02.23

This page has been translated by machine translation. View original

Introduction

I deployed a two-player cooperative browser game using only Vercel (Next.js) and Ably (real-time Pub/Sub).

I had previously tried implementing a competitive game with Vercel + Supabase, and this time I wanted to see how easily I could create a real-time co-op game using Ably with the same approach. I created a 2P co-op action game where players defeat hordes of enemies while leveling up, aiming to defeat a boss that appears after 3 minutes.

vercel-ably-gameplay-gif

What is Vercel

Vercel is a hosting platform for Next.js. In addition to delivering static files, it can run API Routes as serverless functions. In this project, Vercel handles frontend delivery and processes Ably Token issuance and room management via API Routes.

What is Ably

Ably is a real-time Pub/Sub messaging service. It provides a SaaS infrastructure for bidirectional message exchange between clients. The SDK handles WebSocket connection management and reconnection processes, allowing developers to focus solely on sending and receiving messages. It works well with use cases requiring bidirectional, low-latency communication like co-op games, which is why I chose it for this project.

Target Audience

  • People who want to easily deploy real-time co-op games in the browser
  • Those unfamiliar with Ably who want to learn how to use it with Next.js
  • Those who want to build real-time communication apps without managing their own servers

References

Architecture Overview

The system structure is as follows:

What's distinctive is that all game logic runs in the browser. Vercel's API Routes only handle Ably Token issuance and 4-digit room code management, without storing game state on the server side. Real-time synchronization between players happens directly through Ably's Pub/Sub channels.

No network communication is used for enemy spawning or movement. Both clients use deterministic pseudo-random number generators (PRNGs) initialized with the same seed value to calculate enemy spawn positions and timing locally. With this mechanism, even with 1,000 enemies on screen, no bandwidth is consumed for enemy coordinate synchronization. Results like experience gain from defeating enemies are communicated as game-events, but since these are discrete events, their impact on bandwidth is minimal.

vercel-ably-1000-enemies-gif

Player Synchronization with Ably

Token Authentication

I'm using token authentication for Ably connections. To avoid exposing API keys in the browser, I issue tokens via Next.js API Route and pass them to the client.

src/app/api/auth/token/route.ts
const ably = new Ably.Rest({ key: apiKey });
const tokenRequest = await ably.auth.createTokenRequest({
  clientId,
  ttl: 3600 * 1000,
});
return NextResponse.json(tokenRequest);

On the client side, I specify an authCallback for connection. The Ably SDK automatically retrieves a new token when it expires, so there's no need to handle reauthentication.

src/lib/ably/NetworkManager.ts
this.ably = new Ably.Realtime({
  authCallback: async (_tokenParams, callback) => {
    const res = await fetch('/api/auth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ clientId: this._clientId }),
    });
    const tokenRequest = await res.json();
    callback(null, tokenRequest);
  },
  clientId: this._clientId,
  echoMessages: false,
});

Channel Design and Messages

Each room uses a single channel named game-room-{code} with two types of messages.

player-state is the player's position and status sent at 50ms intervals (about 20fps). The JSON payload is about 100 bytes, but the actual measured value including Ably's protocol overhead averaged 204 bytes per message. The receiving side uses linear interpolation (Lerp) to smoothly incorporate the data into 60fps rendering.

{
  "id": "p1",
  "pos": [120.5, 340.2],
  "vel": [1.0, 0.0],
  "hp": 85,
  "dead": false,
  "lv": 5,
  "seq": 142,
  "ts": 1740200000000
}

game-event represents discrete events like game start, player death, and experience gain. These are only sent when they occur, so their frequency is low and their impact on bandwidth is minimal.

Enemy Synchronization with Deterministic PRNG

In co-op games, enemy states need to be consistent between both clients. However, sending coordinates for 1,000 enemies every frame is impractical. To solve this problem, I used a deterministic pseudo-random number generator (PRNG). The Mulberry32 algorithm always produces the same sequence of random numbers when given the same seed value.

src/lib/game/utils/RNG.ts
export class Mulberry32 {
  private state: number;

  constructor(seed: number) {
    this.state = seed;
  }

  next(): number {
    let t = (this.state += 0x6d2b79f5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  }
}

At game start, the seed value is shared via the GAME_START event, and both clients initialize identical Mulberry32 instances. From then on, enemy spawning in each frame is calculated locally based on the same random number sequence, so no network communication occurs for enemy spawning and movement.

Actual Communication Volume

Here are the measured values obtained during the testing period using the Ably CLI (ably apps stats). These are the results after dozens of test plays.

Item Value
Message count (total during testing period) 94,560
Data volume (total during testing period) approx. 19.3 MB
Average message size approx. 204 bytes
Peak concurrent connections 4

After dozens of plays, the message count was about 95,000. This calculates to several thousand messages per play, making it impractical to operate in production with Ably's Free Tier (6 million messages per month). However, the Free Tier is sufficient for prototype testing purposes.

Ably's console includes a Global usage screen to check communication volume and a Live stats screen to monitor connection status in real-time, which are useful for checking current status and making estimates.

ably console global usage

About Perceived Latency

When actually playing co-op with 2 players, we hardly noticed any lag throughout the game. Enemy spawns and experience reflection don't suffer from delay effects since they're processed through local calculations or discrete events as mentioned earlier.

However, there were moments when the other player's movement appeared to warp momentarily. While the receiving side uses linear interpolation (Lerp) to smoothly connect coordinates, if network fluctuations delay the arrival of the next coordinate, when it does arrive, the interpolation destination jumps, making the movement appear momentarily faster. This phenomenon can occur with any real-time communication infrastructure, not just Ably.

In real-time communication, client-side interpolation processing to prevent users from feeling network instability is crucial. While we only used linear interpolation in this project, there's room for improvement with techniques like predictive movement using velocity vectors or buffering to absorb arrival timing jitter. How well you maintain the visual experience locally is just as important as choosing the right communication infrastructure.

Conclusion

With the Vercel + Ably configuration, I was able to implement real-time synchronization and deployment of a 2P co-op browser game without server management. Ably provides the Pub/Sub infrastructure, allowing us to focus on game development without worrying about building and operating WebSocket servers.

Ably isn't the only option for real-time communication. There are combinations like Vercel + Momento, Vercel + Supabase Realtime, each with different characteristics. If caching is your primary use, Momento might be best; if database integration is necessary, Supabase could be suitable; and if bidirectional real-time messaging is central, Ably would be appropriate. It's best to choose based on your specific requirements.

Share this article

FacebookHatena blogX