aymengharbi

live in prod, no console errors (yet)

WebSockets, from zero

A phone call between your browser and a server, and how to keep it safe

On this page
  1. A tiny network course
  2. HTTP is a letter, WebSocket is a phone call
  3. The handshake: asking to switch to a phone call
  4. ws or wss: a postcard or a sealed envelope
  5. Proxies: the receptionist of the building
  6. What can you trust?
  7. How to protect the site
  8. How to choose where to host it
  9. The real numbers of my game
  10. What the front needs
  11. What the back needs
  12. Questions for both
  13. Test before you commit
  14. The whole picture
  15. Remember

Think about the last time you ordered food, then called the restaurant to ask where your order was. The first is a short exchange. The second is a live conversation. A WebSocket is the second one, for computers. This post explains it with everyday situations, using the multiplayer game server from my Bloodstrike project as the real example.

A tiny network course

Before the WebSocket, five words. Each one has a place in your daily life.

  • IP address: the street address of a building. It tells the network which computer to reach.
  • Port: the door number inside the building. One computer can run many services, and each one listens at its own door. Web pages use door 443 (secure) or 80.
  • Packet: a small envelope. A big message is cut into many envelopes. The network carries them one by one.
  • TCP: a postal service with tracking. It makes sure every envelope arrives, in the right order. If one is lost, it sends it again.
  • Client and server: you are the client. The server is the shop that answers you.
  YOU (client)                         SHOP (server)
  address: 82.10.4.7                   address: 203.0.113.5
        │                                   │
        │   envelope ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ door 443
        │◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ envelope    │

HTTP is a letter, WebSocket is a phone call

Normal web pages use HTTP. It works like letters. You send a request. The server sends one answer. Then the exchange is over. If you want news, you must send a new letter.

Now imagine a game. Fifty times per second, the server must tell you where the other players are. Writing fifty letters per second would be slow and silly. Each letter also repeats the same long header with your address and your details.

A WebSocket is a phone call. You dial once. The line stays open. Both sides can talk at any time, and nobody has to ask first. The server can say “Player 2 just moved” the moment it happens.

  HTTP  (letters)                 WebSocket  (phone call)

  client ──request──▶ server      client ══════════════ server
  client ◀─response── server              ◀── "Player 2 moved"
  client ──request──▶ server              ──▶ "I shot left"
  client ◀─response── server              ◀── "You were hit"
        (each time: new letter)          (one line, always open)

This is why chat apps, online games, live prices and shared documents use WebSockets.

The handshake: asking to switch to a phone call

Here is the surprise: a WebSocket does not start as a call. It starts as a letter. The browser sends a normal HTTP request that says: “Can we stop writing letters and talk on the phone instead?” This first exchange is called the handshake. A handshake is a polite agreement before the real talk, like two people who shake hands before a meeting.

Here is the real request, shortened:

GET /ws HTTP/1.1
Host: game.example.com
Origin: https://play.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

Read it like this:

  • Upgrade: websocket means “I would like to change to a phone call.”
  • Origin means “I am calling from this website.” The browser adds it. This line matters a lot for security, as you will see.
  • Sec-WebSocket-Key is a random note. The server must use it in its answer.

If the server agrees, it answers:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

Code 101 means “OK, we switch.” The server builds Sec-WebSocket-Accept from your note. This proves that the server really understands WebSockets, and that you are not talking to an old cache. It is not a security lock. Anybody can compute it.

After this, the letters stop. The same connection stays open and carries small messages both ways.

ws or wss: a postcard or a sealed envelope

There are two kinds of address:

  • ws:// is a postcard. Everyone who touches it can read it: the café Wi-Fi owner, your internet provider, anyone on the path.
  • wss:// is a sealed envelope inside a locked box. Nobody on the road can read it or change it.

The “s” means “secure”. It adds TLS, the same system that gives the padlock in your browser. TLS does three jobs.

  1. Privacy. The content is scrambled. A stranger on the café Wi-Fi sees only noise.
  2. Integrity. If someone changes one letter on the way, the receiver notices.
  3. Identity. The server shows a certificate. It is like an ID card. A trusted office (a certificate authority) checked it and signed it. Your browser keeps a list of trusted offices. If the card is fake, you get a warning.
  ws://   you ───── "my password is 1234" ─────▶ server
                          ▲
                   a stranger reads it, or edits it

  wss://  you ═════ #@!%&*()_+~^ ═════▶ server
                          ▲
                   a stranger sees only noise

There is a practical rule too. A page opened on https:// is not allowed to open a ws:// connection. Browsers block it, except on localhost. So a real site must use wss://. In the game, the deployment guide says the same: TLS must sit in front of the server.

One more thing. TLS does not make the user honest. It protects the road, not the traveller. A cheater can still use a locked box to send bad messages. We come back to this below.

Proxies: the receptionist of the building

On the internet, you often do not reach the real server directly. You first reach a proxy: a computer that stands between you and the server.

Think of an office building. You do not walk into the manager’s room. You speak to the receptionist. The receptionist checks you, then calls the right person, and brings the answer back. This is a reverse proxy. It protects the servers behind it. It can also hold the TLS certificate, so the servers inside do not need one. In my project, the game server speaks plain ws:// and the proxy turns it into wss://.

  browser ══wss══▶ [ proxy / receptionist ] ──ws──▶ game server
                    · holds the certificate         (inside, hidden)
                    · hides the real server
                    · can block bad visitors

A proxy for WebSockets must do three things:

  • Pass the “upgrade” request. If the receptionist throws away the line “I want to switch to a phone call”, the call never starts.
  • Not hang up too early. Many proxies close a connection that is silent for 30 or 60 seconds. A phone call with nothing said looks dead to them. So the server sends a tiny ping every 10 seconds, like saying “Are you still there?” The client answers with a pong.
  • Tell the server who is calling. Because of the receptionist, the server sees only the receptionist’s address. The proxy writes the visitor’s real address on a note: the header X-Forwarded-For. The server reads that note.

What can you trust?

This is the most important question in the post. Use one picture: a bank counter.

The bank gives you a form. You write on it. The clerk does not simply believe you. If you write “Balance: 1,000,000”, the clerk checks the real balance in the bank’s own books.

The same rule works here: everything that comes from the client is only a claim. The server must check it.

  • A message from the player (“I am at x=10, I have 500 health”): no. The player controls their own computer and can send anything.
  • The Origin header: only for browsers. Browsers set it honestly. A script on a laptop can write any Origin it likes.
  • X-Forwarded-For: only if your own proxy wrote it. If clients can reach the server directly, they can write any note.
  • The certificate on a wss:// site: yes, once the browser checked it. A trusted office signed it.
  • The server’s own memory (health, positions, score): yes. It is the one source of truth.

This last line is why the game is server-authoritative. The server is the referee. The player says “I press fire” and the referee decides whether the shot hits. The player can never say “I hit, give me the point”. Cheating clients lose their power.

The X-Forwarded-For line needs care. The setting TRUST_PROXY=1 tells the server to believe the note. Turn it on only behind a proxy you control. Otherwise a stranger writes a fake note, and the server believes it. Turn it off behind a proxy, and every player looks like the receptionist, so the per-person limits hit everybody together.

How to protect the site

Each defence below answers one real danger.

1. Someone hijacks your session from another website. Picture a fake pizza flyer in your mailbox that says “Call this number to confirm your bank transfer”. You are logged in at the bank, and the flyer tricks you into making a call in your name. On the web, a bad page can open a WebSocket to your server from the victim’s browser. This attack is called Cross-Site WebSocket Hijacking. Browsers do not block it by themselves. The defence is the Origin check: the server reads Origin during the handshake and answers 403 Forbidden if the site is not on the allowed list. In the game, the list is ALLOWED_ORIGINS, with the exact address and no extras. Remember that this protects visitors from bad websites. It does not stop a script written by an attacker, so it never works alone.

2. Someone floods you. A person who sends a thousand messages per second is like a crowd pushing into a shop. Set limits:

  • a maximum size per message (2 KiB in the game),
  • a maximum number of messages per second,
  • a maximum number of connections per person (8 per IP address),
  • a maximum number of rooms (50),
  • a limit on chat messages.

3. Someone guesses a password. Like a thief trying every number on a padlock. Allow 5 tries per minute, then lock for a while. Store the password only as a hash, a one-way scramble. A hash is like a fingerprint: you can compare two fingerprints, but you cannot rebuild the finger. Use a slow hashing method (the game uses scrypt) and compare in constant time, so the speed of the answer gives no clue.

4. Someone sends harmful text. A chat message can contain <script>...</script>. If a page prints it as is, that code runs in other people’s browsers. Always treat messages as plain text, and escape them: turn special symbols into harmless ones. Check the size and the type of every field before you use it. Never put user text inside a database query: pass it as a separate parameter.

5. Someone reads the traffic. Use wss:// only. Close the plain ws:// port to the public. Give it only to your proxy.

6. Someone learns too much. The public room list must never contain passwords or private data. Do not write passwords or chat text in the server log. Keep only what you need, for as short as you need. For a game, that can be nothing at all after the server restarts. This keeps you in line with privacy rules such as GDPR.

7. Someone is not allowed in. If part of your site needs a login, check the login before you load or show anything. Like a security guard at the door of the staff room: the guard checks first, then opens.

8. An old part is broken. Install the packages from the lock file, run npm audit, and update the ones with known holes. A strong door does not help if the window is open.

How to choose where to host it

A host is the building where your site lives. Choosing one is like choosing premises for a business. A shop window and a call centre do not need the same building. So a WebSocket project needs two choices: one for the front and one for the back.

  • The front is the shop window. It is the web page that people download: buttons, menus, the game screen. It does not change from one visitor to the next. You can copy it to many places, close to each visitor, like a poster printed in every city.
  • The back is the call centre. It is the game server that keeps the phone lines open. It remembers who is in which room. It must stay at its desk all day.
  FRONT (poster, copied everywhere)    BACK (call centre, one desk)
  ┌───────────────────────────┐        ┌───────────────────────────┐
  │ HTML, JS, images          │        │ one process, always on    │
  │ same for everyone         │        │ holds open connections    │
  │ can be copied near users  │        │ keeps rooms in memory     │
  └───────────────────────────┘        └───────────────────────────┘

The real numbers of my game

The lists below are general. First, here is what my game server needs, with its real numbers. Think of a taxi driver: you do not choose a car only because it has four wheels. You ask how fast it goes, how far it goes, and whether it can wait for you.

  • It must run as a Docker container. Docker packs the program and everything it needs in one box (an image), like a lunch box that is the same in every kitchen. The host must run my box as it is. The image uses Node.js, runs as a normal user (not as the boss of the machine), listens on port 8080 and has a health check on /health.
  • It must answer fast. The server counts time in “ticks”. It plays 60 ticks per second, so one tick lasts about 16.7 milliseconds. Each tick must cost much less than that (my goal is under 2 ms). It sends a picture of the game to each player 20 times per second. A slow or shared machine that pauses for 50 ms makes players jump on screen. So I need a steady processor, not a busy one that is shared with too many neighbours. The host should also be close to the players: every 1,000 km adds a delay that no code can remove.
  • It must keep its memory. Rooms, players and scores live in the memory of the program. There is no database. If the host moves the program to another machine, or restarts it to “save money”, every match ends. This is like a waiter who keeps all the orders in their head. If you replace the waiter in the middle of dinner, all the orders are lost.
  • It must keep the lines open. A match lasts 3, 5 or 10 minutes. The WebSocket must stay open for the whole match, and the host must never close it for its own reasons (no “shutdown” after a few seconds, no limit on the length of a connection). The server sends a ping every 10 seconds. So the host’s idle timeout must be higher than 30 seconds, or it hangs up the call between two sentences.
  • It must forgive short cuts. If a player loses the network, the server keeps their place for 20 seconds. The player’s browser comes back by itself with a secret token. This only works if the program is still alive and in the same memory after the cut. So a host that restarts the container, or sends the player to another copy, breaks this feature.
  • It must stay a single copy. Only one instance of the server. No auto-scaling.
  • It must speak wss://. The server itself speaks plain ws://. The host or a proxy in front must add TLS.
  • It must pass the right headers. Upgrade for the handshake, and X-Forwarded-For for the visitor’s address (with TRUST_PROXY=1 only if the proxy is yours).
  • It must stop politely. When the host stops the container, it sends a SIGTERM signal. The server closes all sockets and exits. The host should allow a few seconds for this, then start the container again if it crashes.
  • It must let me set variables. ALLOWED_ORIGINS (the exact address of the front), TRUST_PROXY, PORT, HOST, MAX_ROOMS and MAX_CONN_PER_IP.
  • It needs little. One room of 8 players is light. My estimate (not measured yet) is about 15 to 20 KB per second to each player. Test before you add many rooms.

In one sentence: I need one always-on Docker container, close to the players, with a steady processor, that never closes my WebSockets and never moves my memory.

What the front needs

Ask these questions before you choose.

  • Can it run my build? A plain static site is only files. A framework such as Next.js may need a small server. Check that the host supports your exact tool and version.
  • HTTPS with no work from me. The certificate must be free, automatic and renewed alone. A page without it cannot open wss:// at all.
  • Where are the copies? A host with many locations is like many shops: each visitor goes to the nearest one. This makes the page load faster.
  • Can I set build variables? Some values, such as the address of the back (wss://...), are fixed when you build. You need a safe place to set them. Never write secrets in the code.
  • Can I go back? If a release breaks, you want a “return to the last good version” button, like an undo key.
  • Does it use my own domain name? Your address should be yours, not the host’s.

What the back needs

This list is longer, because a WebSocket server is more demanding.

  • A process that never goes to sleep. Some hosts run your code only when a request comes in, then stop it. That works for letters. It breaks a phone call, because the person who answers goes home after each sentence. You need a host where your program runs all day.
  • WebSocket support from end to end. Every proxy on the road must pass the “upgrade” request. Ask the host: “Do you support WebSocket connections?” and look for it in the documents.
  • A long idle timeout. The host must not hang up a quiet line too early. It should wait longer than your ping delay (more than 30 seconds when you ping every 10).
  • TLS handled for you. The host should turn ws:// into wss:// with a valid certificate, so your server code stays simple.
  • One copy, or a plan for many. My game keeps its rooms in memory. If the host starts a second copy, it is a different world, and players cannot find each other. Either run exactly one copy, or design shared storage and sticky routing (the same player always goes to the same copy). Turn off auto-scaling until you have done this.
  • Safe settings. You need a place for environment variables (ALLOWED_ORIGINS, PORT, keys). Do not write them in the image or in the repository.
  • Restart and health check. If the program crashes, it must start again alone. The host should call a /health address to know that the program is alive.
  • Logs. You need to read what happened, without writing private data in them.
  • Real client address. The host’s proxy must send the visitor’s address in X-Forwarded-For, and you must know that only this proxy can write it.
  • Enough power, and a clear price. Count the memory, the processor and the open lines. Check how the host charges: some charge per open connection or per minute, which can surprise you with a call centre that never closes.
  • Where the data is. If visitors live in Europe and you handle personal data, check the region of the servers and the privacy terms (GDPR). Check what you store: names, chat, addresses.

Questions for both

  • Same team or not? Front and back may sit at different hosts. That works, if the back allows the front’s address in its list of allowed origins.
  • Can I deploy both together? The front and the back share rules. If you release them at different times, players can see strange jumps or errors.
  • Is it supported and trusted? Look at uptime history, support, and how easy it is to leave. Moving out should not mean rewriting your project.
  • What does it cost at the end? Add the host, the traffic, the domain and your own time.

Test before you commit

Do not trust a feature page alone. Run a short trial, like a test drive.

  1. Deploy a tiny server that answers /health.
  2. Open a WebSocket to it with wss:// from your real front.
  3. Leave it silent for two minutes. Does it stay open?
  4. Send a request with a wrong Origin. Does it answer 403?
  5. Restart it. Does it come back alone?

If your company or your school has rules about where you may host, follow them first. Ask the team that owns those rules before you pick a provider.

The whole picture

  Player's browser
        │  1. HTTPS: loads the page
        │  2. handshake: "switch to WebSocket?"  (Origin sent)
        ▼
  ┌─────────────────────────┐
  │ Proxy (receptionist)    │  · TLS on, ws → wss
  │                         │  · passes Upgrade, long timeout
  └────────────┬────────────┘  · writes X-Forwarded-For
               ▼
  ┌─────────────────────────┐
  │ Game server (referee)   │  · checks Origin  → else 403
  │                         │  · limits: size, rate, per person
  │                         │  · validates every message
  │                         │  · owns the truth: health, score
  └─────────────────────────┘

Remember

  • HTTP is a letter. A WebSocket is a phone call that stays open.
  • The handshake is the polite question that starts the call.
  • Use wss://. A postcard is not safe.
  • A proxy is a receptionist. Trust its notes only if it is yours.
  • The client is only a claim. The server decides.

Next time you play online or use a live chat, think about the open line, the receptionist and the referee. They are all there.