How this page was served

This page is not a static file. The function rendered it for your request, just now, in the execution environment below. Refresh a few times: with no session affinity, the environment ID can change, and the request count goes up.

Live facts about this request

Execution environment0e0bd6e8
Regionus-east-1
This request waswarm this environment had served requests before
Environment started2026-10-21T14:46:44.012Z (up 57 min 55 s)
Requests served by this environment274
Games run here / copies / spectators0 / 0 / 0
hostmbnd5msy2zhmrrzd.3t7.lambda-web.us-east-1.on.aws
user-agentMozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
CloudFrontyes, request ltnY0nggCJ9Jm-6cu9I0z59AnkLS_BuSASqOfI9yiuz0JRssSEqs9g== via 2.0 a10a7384ec53ee4c3ce439b9f86213b2.cloudfront.net (CloudFront)
x-forwarded-for216.73.217.x, 3.172.126.x
Render time1.15 ms (also in the server-timing header)

Counters are per execution environment and reset when it starts. Addresses have their last part hidden.

Architecture

Architecture diagram: browser, CloudFront on the custom domain, Web Function endpoint, execution environments running Doom in WebAssembly, VPC egress through a network connector, and the DynamoDB table holding leases and input logs

Step by step

  1. The endpoint routes your request to an execution environment. Your browser called CloudFront, which forwarded the request to the Web Function endpoint (mbnd5msy2zhmrrzd.3t7.lambda-web.us-east-1.on.aws, endpoint live). Lambda picked an execution environment for it, here 0e0bd6e8. There is no session affinity: the next request, even from this tab, may land on a different one.
  2. A plain node:http server answers. A Web Function runs an ordinary HTTP server. The runtime sets AWS_LAMBDA_HTTP_ENDPOINT and the server listens on it, no handler wrapper:
    server/index.js:40-41
    // On Lambda the runtime sets AWS_LAMBDA_HTTP_ENDPOINT. Locally, loopback only.
    const [HOST, PORT] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || `127.0.0.1:${process.env.PORT || 3000}`).split(':');
    server/index.js:690-690
    server.listen(Number(PORT), HOST, () => console.log(`freedoom-lambda-web-functions environment ${ENV_ID} listening on http://${HOST}:${PORT}`));
  3. The router reaches this route and renders the HTML. The request handler counted your request, matched /how-it-works and called renderHowItWorks(). The server-timing response header carries the render time.
    server/index.js:561-571
      if (req.method === 'GET' && url.pathname === '/how-it-works') {
        const t0 = performance.now();
        const html = renderHowItWorks(req, {
          t0, cold, environment: ENV_ID, region: process.env.AWS_REGION || 'local', metrics: metrics.snapshot(),
          games: games.size, followers: followers.size, spectators: spectatorCount(),
        });
        res.writeHead(200, {
          'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store',
          'server-timing': `render;dur=${(performance.now() - t0).toFixed(2)}`,
        });
        res.end(html);
  4. For the game, the same server does everything else.
    • The page and its script are files served from a map:
      server/index.js:520-529
      const STATIC = {
        '/': ['index.html', 'text/html; charset=utf-8'],
        '/index.html': ['index.html', 'text/html; charset=utf-8'],
        '/client.js': ['client.js', 'text/javascript; charset=utf-8'],
        '/qrcode.mjs': ['qrcode.mjs', 'text/javascript; charset=utf-8'], // qrcode-generator 2.0.4, MIT
        '/pricing': ['pricing.html', 'text/html; charset=utf-8'],
        '/pricing.js': ['pricing.js', 'text/javascript; charset=utf-8'],
        '/pricing-model.js': ['pricing-model.js', 'text/javascript; charset=utf-8'],
        '/architecture.svg': ['architecture.svg', 'image/svg+xml'], // copied from docs/architecture.svg by make diagram
      };
    • The game runs here, in WebAssembly, and frames stream back on one long response:
      server/index.js:411-414
      // One frame on the wire, inside a single deflate stream per connection:
      //   u32 tic | u8 flags (bit 0: palette follows) | 768 bytes palette | w*h pixels XOR previous frame
      // XOR against the previous frame turns unchanged pixels into zeros, which deflate
      // compresses to almost nothing.
    • Web Functions don't run WebSockets, so key presses come back as separate POSTs. If the game runs elsewhere, the input goes to a DynamoDB inbox:
      server/index.js:674-677
            const local = games.get(id);
            try {
              if (local) local.offer(cid, seq, keys);
              else await store.putInbox(id, cid, seq, keys);
    • Only one environment runs a game at a time. The owner writes its input log and heartbeat in one transaction that only succeeds while it holds the lease:
      server/store.js:200-225
        async heartbeat(id, me, gen, { tic, seqs, firstTic, events }) {
          const items = this.logPut(id, firstTic, events);
          items.push({
            Update: {
              TableName: this.table, Key: this.key(id, 'lease'),
              UpdateExpression: 'SET heartbeatAt = :now, tic = :tic, seqs = :seqs, expiresAt = :exp',
              ConditionExpression: '#o = :me AND gen = :g AND attribute_not_exists(want)',
              ExpressionAttributeNames: { '#o': 'owner' },
              ExpressionAttributeValues: {
                ':now': toAttr(Date.now()), ':tic': toAttr(tic), ':seqs': toAttr(seqs),
                ':exp': toAttr(expires(GAME_TTL_S)), ':me': toAttr(me), ':g': toAttr(gen),
              },
              ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
            },
          });
          try {
            await this.client.send(new this.ddb.TransactWriteItemsCommand({ TransactItems: items }));
            return { ok: true };
          } catch (e) {
            if (e.name !== 'TransactionCanceledException') throw e;
            const reason = e.CancellationReasons?.[items.length - 1];
            const current = leaseFrom(reason?.Item);
            if (current && current.owner === me && current.gen === gen && current.want) return { want: current.want };
            throw new LeaseLost(`lease for ${id} moved to ${current?.owner} gen ${current?.gen}`);
          }
        }
    • When a stream lands on another environment, that one takes the game over and rebuilds it by replaying the input log. The engine is deterministic, so it lands on the same tic:
      server/index.js:219-227
      async function rebuild(id, lease, handoff) {
        const t0 = performance.now();
        const engine = new DoomEngine();
        const events = await store.readLog(id);
        const { palette } = await replay(engine, events, lease.tic);
        const info = { replayedTics: lease.tic, replayMs: Math.round(performance.now() - t0), handoff };
        console.log(JSON.stringify({ msg: 'rebuilt', game: id, ...info, events: events.length }));
        return start(id, engine, lease, { ...info, palette });
      }

Rendered by server/howitworks.js. See pricing for what running this costs.