SAM, Sam's Web Site

Team Meeting Bingo, and why Lambda Web Functions matter

 
Evergreen
intro

AWS gave Lambda a way to run your own web server and hold a connection open. I built a joke bingo app to find out what that's actually for.

TL;DR
Lambda Web Functions run your own HTTP server and keep streams open, so a live board, queue or Q&A becomes a Lambda deploy rather than a WebSocket project. Know the limits before you pick one.

A joke app about the worst bits of meetings, sat on top of something AWS launched this week that isn't a joke at all.

That's twelve bots playing bingo against a scripted team meeting. Every square is something you've probably heard this week: "Sorry, I was on mute." "Can everyone see my screen?" "This could have been an email." - well that last one is likely thought rather than said out loud. When somebody says one, every card with that square lights up, on every screen, as it happens.

So here's one I made earlier, and it's daft on purpose !

Anyone who knows me knows I'm bullish on Lambda and serverless. I got my start on AWS with Amazon Connect, automating IVRs (the "press 1 for billing" phone menus), so when I read about Lambda's new web functions I knew I wanted to see what I could build with them. I spent the last two evenings on this.

The thing underneath it launched on 12 October, and it changes what a Lambda function can be. So this post is about that, and about where I'd use one for real. (Spoiler: it's not the bingo app.)

What a web function actually is

The Lambda you know, now called an event function, gets an event, does a thing and hands back a result. One request per environment at a time, then it's done.

A web function runs your own web server instead. You write a perfectly ordinary HTTP server, it listens on the port Lambda hands it, and Lambda runs as many copies as the traffic needs. Each copy is an environment.

That changes more than it sounds like it should. One environment now handles lots of requests at once; in my runs a single environment peaked at 64 in flight, which was the ceiling I'd set. And a response can stay open, so the server keeps writing to the browser instead of answering and hanging up. That's how every card updates live. Each browser holds one stream open (Server-Sent Events, the plain HTTP kind) and the server pushes changes down it.

The catch is that there's no session affinity. Every request is routed on its own, so two requests from the same browser can land on different environments. Watch the badges in the GIF: twelve players spread over five environments, and no telling who lands where.

You deploy it as a function, a revision (an immutable snapshot of your code and settings) and an endpoint. The endpoint is the HTTPS address. It decides who's allowed to call it and how traffic splits between revisions, so canary releases come built in.

Why you'd care

Live apps on AWS have always been a bit of a project. If you wanted a page that updates itself, the options looked like this.

OptionWhat it costs you
API Gateway WebSocketsA table of connection IDs, a function per message, and code to post back to every connection. It works, but you're building plumbing.
A container on ECS or FargateEasy to write. Also sat there running at 3am when nobody's watching.
PollingEvery browser asks "anything new ?" every few seconds, and you pay for every time the answer is no.

A web function gives you the container way of writing it, with Lambda doing the scaling. The bingo server is one node:http file with no framework, and it has no idea it's on Lambda. On my laptop it runs against DynamoDB Local. In AWS it's the same code.

Pricing is per request ($0.30 a million), per active vCPU-second ($0.0000276944) and per GB-second of memory ($0.0000036667).* You only pay for CPU while your code is using it, but memory is charged for as long as the environment is up, so a stream sat open waiting for the next phrase costs you mostly memory.

US East (N. Virginia) rates; Ireland pricing wasn't listed when I checked.

What this looks like in production (not a demo)

Take the jokes out and bingo is a pattern: lots of people watching the same thing change, live, with the occasional stampede. That pattern is everywhere. None of these are things I've built. They're where I'd reach for a web function first, and why.

WhereWhat's liveWhy it fitsThe bingo version
Local governmentElection count night: a results board as each ward declaresQuiet for hours, then everyone refreshes at once on every declarationA phrase gets said and every card updates
EducationResults day and clearing placesEveryone turns up in the same few minutes, once a yearHundreds of players joining at once
HealthcareA&E or clinic waiting-time boardOne number that lots of people watch all dayThe live player count
Events and retailYour place in the queue for a ticket dropA burst of joins, then a long wait watching a number moveThe lobby filling up
Any organisationAll-hands Q&A and live pollingHundreds of people watching one screen while the votes landLiterally this app, minus the jokes
TransportA live disruption boardUpdates pushed as they happenPhrases "said" on the meeting clock
Anything with AI in itA model's answer streamed as it's writtenLong responses that would otherwise sit behind a spinnerThe event stream itself

The steady state is easy in every one of these. It's the stampede that hurts. Keep that in mind for the end of this post.

How it's wired

Team Meeting Bingo architecture: browsers to CloudFront and WAF, signed requests to the web function endpoint, environments, and DynamoDB

Following the numbers:

  1. Browsers talk HTTPS to CloudFront: pages, API calls, and one stream per player that stays open.
  2. AWS WAF rate-limits each IP address. A public live app is a magnet for scripts, so this goes in from day one.
  3. CloudFront signs every request it sends to the endpoint, and the endpoint uses IAM auth. Like every web function endpoint it's reachable from the internet, but it refuses anything unsigned, so nobody gets round CloudFront and the WAF.
  4. The endpoint spreads requests over environments. No affinity means no environment "owns" a game.
  5. Joins, marks and wins are conditional writes to DynamoDB, so the rules hold whichever environment takes the request. You can only mark a phrase that's been said, and there's only ever one winner. Each environment reads each game it's streaming once a second.
  6. Changes go back down every open stream.

Nothing in an environment matters if it disappears. Everything that does matter lives in DynamoDB, and the meeting script is worked out from the clock, so every environment agrees on what's been said without ever talking to the others.

When not to reach for one

Should everything move to web functions ? It depends. Here's where I wouldn't.

  • You need two-way, real-time traffic. There are no WebSockets, and a request can't stream both ways at once. A multiplayer game with constant input belongs somewhere else.
  • Your app keeps state in memory. There's no session affinity and no promise an environment sticks around, so move the state out first.
  • It's event-driven anyway. An S3 thumbnail resizer is still an event function, and should stay one.
  • You need a connection that never closes. Every request has a timeout, streams included (15 minutes at most), so clients have to reconnect. A browser's EventSource does that for you, mostly.
  • You're tied to a Region. At launch it's N. Virginia, Ohio, Oregon, Tokyo and Ireland. London isn't on the launch list.
  • You need the governance tooling on day one. At launch, AWS Config, Security Hub controls and CloudFormation drift detection don't cover web functions. In a regulated estate, have that conversation before the first deploy.

If you do use one, set both guard rails on the endpoint: a cap on environments and a requests-per-second limit. Anything over either gets a 429, which beats finding out from the bill.

Then I sent 500

Twelve players is where a normal demo stops. Twelve tiles, five environments, one winner, round of applause.

So I sent 500 bots at it.

271 got in. The other 229 got an HTTP 500.

Before anyone starts the "Lambda doesn't scale" thread: Lambda was the best-behaved thing in the stack. It scaled and it answered every request. The error it handed back was mine, because the bug was in how I'd stored the game, and AWS did exactly what its documentation says it will - but whos got time to read the documentation!

Part 2 is the post-mortem: why 500 players broke it, why sending 1,000 got exactly two more in, and how I fixed it properly.


I designed and architected this one; Claude Code did a lot of the typing and most of the log trawling. The code is on GitHub. And if your Thursday all-hands needs livening up, you know where it is.

Key takeaways
  • A web function runs your own ordinary HTTP server; one environment handles many requests at once and can hold a response open as a stream.
  • There's no session affinity, so any state that matters has to live outside the environment, in something like DynamoDB.
  • Live boards, queues, results nights and all-hands Q&A fit well: lots of people watching one thing change, with the occasional stampede.
  • Not for two-way real-time traffic, in-memory state, connections that never close, or estates that need AWS Config and Security Hub coverage on day one.
  • Set both guard rails on the endpoint, an environment cap and a requests-per-second limit, so overload is a 429 rather than a bill.
SAM (Sam's Web Site) is a parody. Not affiliated with, endorsed by, or in any way the fault of Amazon Web Services.