<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Cookies, cache and websockets: A practical Guide for developers

Cookies, cache and WebSockets are three fundamental mechanisms of every modern web application, yet they are often configured by trial and error, without really understanding their interactions. A misconfigured cookie exposes you to XSS/CSRF; a poorly set cache serves stale content or, worse, private data to other users; a WebSocket without authentication or scaling strategy collapses at the first peak of traffic. This guide covers the three topics in a practical way: cookie security attributes, HTTP header caching and CDN strategies, WebSocket handshake and scalability with Redis pub/sub, and the patternsnapshot + deltawhich makes them work together in high-performance real-time applications.

Why cookies, cache and WebSockets matter together

These three mechanisms do not exist in isolation: a session cookie authenticates the WebSocket connection during the handshake phase; an HTTP response cached too aggressively can hide data that should arrive in real time via WebSocket; a CDN that does not properly distinguish authenticated requests risks serving one user's private content to another. Understanding the interactions between the three levels is more important than knowing them individually.

Cookies: types, attributes and security

A cookie is a small portion of data that the server asks the browser to store and resend on each subsequent request to the same domain. They are the basis of most web session and authentication systems, but also one of the most common attack surfaces if configured without the right attributes.

Essential security attributes

  • HttpOnly: prevents access to the cookie via JavaScript (document.cookie), mitigating session theft via XSS.
  • Secure: The cookie is only sent over HTTPS connections, never in the clear over HTTP.
  • SameSite: Controls whether the cookie is sent in cross-site requests.Strictit is the safest but breaks some navigation flows from external links;Laxis the reasonable default for session cookies;It is not(requiresSecure) is only needed for explicit cross-site cases (e.g. embedded widgets).
  • Max-Age / Expires: explicit lifespan; without them the cookie is "session" and disappears when the browser is closed.

Snippet 1 — Set a secure session cookie in Express

app.post('/login', async (req, res) => {
  const { accessToken } = await authService.login(req.body);

  res.cookie('session_token', accessToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 15 * 60 * 1000, // 15 minuti
    path: '/',
  });

  res.json({ success: true });
});

Consent management and privacy (GDPR)

Cookies onlytechnically necessary(session, security, load balancing) can be set without explicit consent. Analytics, marketing or profiling cookies require a consent banneropt-inactive before being written, with a register of preferences that can be consulted and revoked at any time. Never set non-essential cookies before the user has expressed an explicit choice.


Cache: levels, HTTP headers and strategies

The cache reduces the load on the server and the response time perceived by the user, but introduces a classic problem:invalidate correctlychanging content. There are multiple cache levels along the path of a request, each with its own logic and headers.

The cache levels in a typical request

LevelWhere he livesMain headers
Cache browserOn the user's deviceCache Control,ETag,Last-Modified
CDNGeographically distributed edge serversCache Control: s-maxage,CDN-Cache-Control
Reverse proxyIn front of the application server (Nginx, Varnish)Cache Control,X-Cache-Status
Application cacheIn-memory or Redis, inside the appManaged at the application level, no HTTP headers

Snippet 2 — Cache header and ETag in Express

app.get('/api/products/:id', async (req, res) => {
  const product = await productService.findOne(req.params.id);
  const etag = `"${product.updatedAt.getTime()}"`;

  res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
  res.set('ETag', etag);

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end(); // contenuto non cambiato, nessun body inviato
  }

  res.json(product);
});

stale-while-revalidateallows the cached version to be served immediately (even if it has expired) while a new version is fetched in the background — an excellent compromise between freshness and perceived speed.

Snippet 3 — Configuring cache in Nginx as a reverse proxy

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m;

server {
  location /api/ {
    proxy_pass http://backend_upstream;
    proxy_cache api_cache;
    proxy_cache_valid 200 1m;
    proxy_cache_use_stale error timeout updating;
    add_header X-Cache-Status $upstream_cache_status;
  }
}

Cache busting and purging CDN

For static assets (JS/CSS with hash in the filename, e.g.main.a1b2c3.js), Thecache bustinghashing the contents in the filename is the most robust strategy: change the filename at every build, hence an aggressive cache (max-age=31536000, immutable) is safe because the URL changes automatically. For dynamic content that needs to be explicitly invalidated, you need apurgetargeted CDN side.

Snippet 4 — Selective purge of a CDN (Cloudflare example)

curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
  -H "Authorization: Bearer ${CF_API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://esempio.it/api/products/42"]}'

WebSocket: handshake, authentication and scalability

The WebSocket opens a persistent two-way connection between client and server, initiated with an HTTP handshake that is "upgraded" to the protocolws://(orwss://over TLS). Unlike HTTP, the connection remains open: perfect for real-time notifications, chat, live dashboards, but requires explicit management of authentication and scalability that stateless HTTP does not pose.

Snippet 5 — WebSocket server with session cookie authentication

import { WebSocketServer } from 'ws';
import cookie from 'cookie';
import { verifySessionToken } from './auth.js';

const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', async (req, socket, head) => {
  const cookies = cookie.parse(req.headers.cookie || '');
  const user = await verifySessionToken(cookies.session_token).catch(() => null);

  if (!user) {
    socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
    return socket.destroy();
  }

  wss.handleUpgrade(req, socket, head, (ws) => {
    ws.user = user;
    wss.emit('connection', ws, req);
  });
});

Snippet 6 — WebSocket client in the browser

const socket = new WebSocket('wss://esempio.it/realtime');

socket.addEventListener('open', () => {
  console.log('Connesso');
});

socket.addEventListener('message', (event) => {
  const { type, payload } = JSON.parse(event.data);
  if (type === 'snapshot') applySnapshot(payload);
  if (type === 'delta') applyDelta(payload);
});

socket.addEventListener('close', () => {
  setTimeout(() => reconnect(), 2000); // riconnessione con backoff
});

Scalability: sticky session and Redis pub/sub

A WebSocket maintains state over the connection: If you have multiple server instances behind a load balancer, a client connected to instance A will not receive messages posted by instance B, unless you share events between the instances. Two approaches:sticky sessions(the load balancer always routes the same client to the same instance, via affinity cookies) eRedis pub/sub(each instance publishes to a shared Redis channel, and all subscribed instances forward the message to their connected clients). Redis pub/sub is the most robust solution because it does not depend on the behavior of the load balancer.

Snippet 7 — Redis pub/sub to sync multiple WebSocket instances

import { createClient } from 'redis';

const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
await publisher.connect();
await subscriber.connect();

// Ogni istanza si sottoscrive e inoltra ai propri client connessi
await subscriber.subscribe('ws:broadcast', (message) => {
  const payload = JSON.parse(message);
  for (const client of wss.clients) {
    if (client.readyState === client.OPEN) client.send(payload);
  }
});

// Qualsiasi istanza può pubblicare un evento condiviso
export function broadcast(event) {
  publisher.publish('ws:broadcast', JSON.stringify(event));
}

WebSocket Security

  • Always WSS(WebSocket over TLS) in production, neverws://not encrypted.
  • Handshake authentication, not after: verify the token/cookie before accepting the connection upgrade, as in snippet 5.
  • Rate limitingon incoming messages to prevent a single client from saturating the server's CPU/bandwidth with a flood of messages.
  • Message validation: Treat every incoming WebSocket message as untrusted input, just like an HTTP body.

Interactions between cookies, cache and WebSockets

Where these three mechanisms intertwine is often the source of subtle bugs in production. A CDN that caches an HTML/JSON response without distinguishing between session cookies can serve one user's data to another (serious privacy violation). A cookieSameSite=Strictset for the primary domain can successfully prevent the WebSocket handshake if the client connects from a different subdomain. A WebSocket used to notify "data has changed" must explicitly invalidate the corresponding HTTP cache, otherwise the realtime updated client will still display stale data on the next page refresh.

Architectural pattern: snapshot + delta

The most effective pattern for high-performance realtime applications combines the three levels: upon initial loading the client requests onesnapshotcomplete via HTTP (cacheable withCache ControlAndETagfor subsequent refreshes), then opens a WebSocket connection to receive only idelta(the incremental changes) from that point on. This drastically reduces traffic compared to sending the entire state on every change, and uses the HTTP cache for the common case (cold loading) keeping realtime only where it is really needed.

Snippet 8 — Client-side snapshot flow + delta

async function initRealtimeView() {
  // 1. Snapshot iniziale via HTTP, sfrutta la cache del browser/CDN
  const res = await fetch('/api/dashboard/snapshot', { credentials: 'include' });
  let state = await res.json();
  render(state);

  // 2. Delta incrementali via WebSocket da questo momento in poi
  const socket = new WebSocket('wss://esempio.it/realtime/dashboard');
  socket.addEventListener('message', (event) => {
    const delta = JSON.parse(event.data);
    state = applyDelta(state, delta); // merge immutabile dello stato
    render(state);
  });
}

Security and privacy: XSS, CSRF and session revocation

HttpOnly cookies mitigate session theft via XSS, but do not prevent it at the source: you still need to sanitize every input rendered in the DOM. For the CSRF, a cookieSameSite=Laxalready blocks most of the most common cross-site attacks; for sensitive endpoints (password changes, payments) add an explicit CSRF token verified server-side, independent of the session cookie.

What to never cache

  • Responses containing data specific to the authenticated user, unless the cache is changed forVary: Cookiesor by authorization header.
  • Login/logout endpoints and any response that sets or invalidates a session cookie.
  • Sensitive data (payment information, tokens, personal data) — never in CDN cache, not even for a few seconds.

For therevocation of sessions, a signed cookie alone is not enough: you need a server-side whitelist/blacklist (Redis is a common choice) that allows you to invalidate a specific token immediately, without having to wait for its natural expiration.


Performance: Combining cache and WebSocket

The snapshot+delta pattern described above is also the main lever for improvementLCP(Largest Contentful Paint) eTTI(Time to Interactive): The initial snapshot, served as an HTTP/CDN cache when possible, arrives much faster than a cold WebSocket round-trip, while the WebSocket only takes care of subsequent updates, reducing both the load on the server (no repeated polling) and network traffic (only delta, not the entire state).

Performance checklist

  • Serve initial snapshot from cache/CDN when content allows, not always from WebSocket.
  • Compress WebSocket messages (permessage-deflate) to significantly sized payloads.
  • Don't open the WebSocket connection before you actually need it: delay it until after the initial rendering to avoid competing with critical resources.
  • Monitor the number of concurrent WebSocket connections per instance and plan horizontal scaling in advance.

Testing and monitoring

Cookies, cache and WebSockets require different testing strategies than those of a traditional REST endpoint: not only functional correctness must be verified, but also behavior under load and over time.

Checklist testing and monitoring

  • Functional tests: check cookie attributes with the browser's DevTools tools (Application → Cookies) and withcurl -Ito inspect response headers.
  • WebSocket load testing: tools likeartilleryork6support WebSocket scenarios to simulate hundreds of concurrent connections and measure message latency.
  • Cache hit/miss ratio: Monitor the headerX-Cache-Status(Nginx) or native CDN metrics; a low hit ratio on content that should be cacheable is a sign of incorrect configuration.
  • Metrics to track: Active WebSocket connections, reconnection rate, p95 message latency, cache hit ratio per endpoint, average cache invalidation time after an event.

Case studies

Case 1 — E-commerce dashboard with real-time stock updates

An e-commerce with40,000 sessions/dayupdated product availability by polling every 5 seconds, generatingpeaks of 8,000 requests/minuteon the backend during peak hours. By migrating to the snapshot+delta pattern (60s cached HTTP snapshot + WebSocket for stock deltas), the load on the backend dropped by73%, the perceived latency for stock updates went from 5s toless than 300ms, and the product page LCP improved from 2.9s to1.8sthanks to the removal of blocking polling.

Case 2 — Support chat with scaling issues

A SaaS application with integrated support chat suffered from lost messages when traffic exceeded a single backend instance (clients connected to different instances did not receive the same events). After the introduction of Redis pub/sub to synchronize messages between4 instancesof the WebSocket server, the rate of lost messages has dropped from2.3% to 0%on a sample of 50,000 messages monitored in two weeks, allowing horizontal scaling without tying clients to fragile sticky sessions.


Common mistakes to avoid

  • Session cookies without HttpOnly: Exposes the token to any XSS script that manages to inject itself into the page.
  • SameSite=None without Secure: Modern browsers silently reject the cookie, resulting in hard-to-diagnose authentication bugs.
  • CDN cache on authenticated responses without Vary: real risk of serving one user's data to another.
  • ETag calculated non-deterministically(e.g. based on generation timestamp instead of data): completely defeats the benefit of 304 Not Modified.
  • WebSocket without handshake authentication: Verifying identity only after connection leaves a window of unauthorized access.
  • No client-side reconnection strategy: A temporary network disconnection becomes a permanent realtime interruption.
  • WebSocket scaling with sticky sessions only: Fragile if the instance is restarted, the client loses the session and must reauthenticate from scratch.
  • No rate limiting on incoming WebSocket messages: A malicious or buggy client can saturate the server's CPU/bandwidth with a flood of events.

Operational checklist and 30/60/90 day plan

PhaseObjectiveReference KPIs
Days 1-30Audit cookie attributes and cache headers on all primary endpoints0 session cookies without HttpOnly/Secure/SameSite
Days 31-60Introduction of the snapshot+delta pattern on the most critical view, cache hit/miss monitoring setupCache hit ratio >80% on cacheable endpoints
Days 61-90Redis pub/sub for WebSocket scaling, load testing, and rate limiting on realtime channels0% message loss under simulated load, p95 message latency < 300ms

Recurring tasks:monitor WebSocket reconnection rate and 401 handshake errors daily; review the cache hit ratio per endpoint every week; each month perform a full WebSocket load test and cookie attribute audit on any new endpoints introduced.


Frequently asked questions

What is the difference between SameSite=Strict and SameSite=Lax?

Strictnever sends the cookie in cross-site requests, not even by clicking a link from another site;Laxsends it for direct navigations (top-level GET) but not for embedded requests such as cross-site images or iframes.

Can I use WebSocket without authentication?

For non-sensitive public data only; for any data linked to a specific user, authentication must be verified at the handshake, before accepting the connection.

What happens if I don't set Cache-Control on a response?

The default behavior varies for browsers and intermediaries; it is always better to be explicit, even to sayno-storeon content that should never be cached.

Do ETag and Last-Modified do the same thing?

Both enable conditional validation (response 304), but ETag is more precise because it is based on the content itself, while Last-Modified has a resolution per second and can give false negatives on very close changes.

Do you always need Redis to scale WebSockets?

With a single server instance there is no need; it becomes necessary as soon as you have multiple instances behind a load balancer that must share the same realtime events between clients connected to different instances.

Do cookies work with WebSocket?

Yes, the browser automatically sends domain cookies during the HTTP handshake request before upgrading to WebSocket, allowing the connection to be authenticated in the same way as a normal HTTP request.

What does stale-while-revalidate mean?

It is a Cache-Control directive that allows you to immediately serve an expired response from the cache, while in the background a fresh version is requested to be used for subsequent requests.

How do I invalidate the cache after an update?

For static assets with hashes in the name, automatically change the URL; For dynamic content served by CDN, an explicit purge towards the specific URL or associated cache tag is needed.

Is WSS mandatory in production?

Yes: without TLS, both the data exchanged and any cookies/tokens used for authentication travel in the clear, exposing the application to interception and manipulation.

How do I handle client-side WebSocket reconnection?

With exponential backoff (increasing waits between retries) and, after reconnection, requiring a new snapshot to align the state, instead of assuming that the deltas lost during disconnection are irrelevant.


6 quick answers for featured snippets and AI assistants

What is the HttpOnly attribute of a cookie?
HttpOnly is a cookie attribute that prevents their value from being accessed via client-side JavaScript (document.cookie). The cookie is still automatically sent by the browser with every HTTP request to the domain, but it cannot be read or stolen by a malicious script injected via an XSS attack.

What is the stale-while-revalidate Cache-Control directive?
It is an HTTP directive that allows the browser or CDN to immediately serve an expired response from cache, while an updated version is retrieved in the background to use for future requests. Improve perceived speed without sacrificing data freshness over time.

How does WebSocket handshake work?
The client sends a normal HTTP request with the headerUpgrade: websocket; if the server accepts, it responds with status 101 and the connection switches from the HTTP protocol to the WebSocket protocol, remaining open and bidirectional until it is explicitly closed by one of the two parties.

Why do you need Redis pub/sub to scale WebSocket?
When multiple WebSocket server instances run behind a load balancer, each instance sees only its own connected clients. Redis pub/sub acts as a shared channel: an event published by one instance is received by all the others, who forward it to their respective connected clients, ensuring that everyone receives the same update regardless of which instance serves them.

What is the snapshot + delta pattern?
It is an architectural pattern for realtime applications: the client first loads a complete state (snapshot) via HTTP, cacheable normally, then receives only incremental changes (delta) via WebSocket from that point on. It drastically reduces traffic compared to sending the entire state on every change.

What is the difference between ETag and Cache-Control?
Cache-Control establishes how long a response can be considered fresh without contacting the server again; ETag is a content identifier used, after expiration, to verify with a conditional request whether the content has actually changed, avoiding retransmitting identical data.


Recommended images and diagrams

DiagramCaptionRecommended size
Snapshot + delta architectureClient flow → HTTP snapshot (cache/CDN) → Incremental delta WebSockets1200×630px
Flow cookie → auth → WebSocketLogin set cookie HttpOnly → handshake WebSocket read cookie → authenticated connection1200×630px
HTTP cache and header levelsBrowser → CDN → reverse proxy → application cache, with the respective headers involved1200×630px
Multi-instance pub/sub Redis flowInstance A publishes an event → Redis → instances B and C receive it and forward it to their clients1200×630px

Terminal output screenshot (e.g.curl -Ion response headers, or the result of a WebSocket load test) should be inserted immediately after the corresponding command snippet, to show the expected output next to the command that generates it.


Structured data and technical SEO

Example JSON-LD Article

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Cookie, Cache e WebSocket: guida pratica per sviluppatori",
  "description": "Guida pratica a cookie sicuri, header di cache HTTP e WebSocket scalabili, con esempi Express/Nginx/Redis.",
  "author": { "@type": "Organization", "name": "Nome Azienda" },
  "datePublished": "2026-08-07",
  "dateModified": "2026-08-07",
  "mainEntityOfPage": "https://www.esempio.it/blog/cookie-cache-websocket"
}

Open Graph tags and recommended URLs

TagsRecommended value
og:titleCookies, Cache and WebSockets: Guide for Developers
og:descriptionSecure cookies, cache headers and scalable WebSockets: practical examples Express, Nginx and Redis.
og:imageDedicated 1200×630px image with the three concepts represented graphically
Recommended URL/cookie-cache-websocket

How to check

  • HTTPS/WSS testing: check withcurl -I https://yoursite.itthat the certificate is valid and that each WebSocket connection useswss://, neverws://, in production.
  • Check cache header:curl -Ion the main endpoints to check thatCache ControlandETagare present and consistent with the chosen strategy.
  • Accessibility test (a11y): verify that the cookie consent banners are navigable by keyboard and readable by screen readers.
  • Bundle size: Check that introducing a WebSocket library (e.g. Socket.IO) doesn't overly bloat the client bundle compared to a simpleWebSocketsnative, if the additional features are not needed.
  • Snapshot test: Verifies that the initial payload via HTTP and the first delta received via WebSocket produce a consistent state, without duplications or holes.
  • Check in CI: Integrate an automated test that opens a real WebSocket connection against a staging environment and verifies handshake, authentication, and receipt of at least one message.

Conclusion: where to start

Cookies, cache and WebSockets should not be approached as three separate problems: their interactions are often the real cause of security or performance bugs in production.Start with an auditof cookie attributes and cache headers on existing endpoints, then introduce the snapshot+delta pattern on the most critical view of your application. If you prefer a direct comparison on your specific case,request a technical auditor download the operational checklist of this guide to immediately start applying it to your project.

💬 Reader notes

0 notes

Write a note

Share your opinion, a suggestion or a compliment

Latest notes

No notes yet. Be the first to comment!