Serverless WebSocket costs: connection minutes rarely dominate
Connection minutes dominated in only 3 of 18 modeled scenarios. Below 1,000 users, $16.64 a month for Redis and a VPC endpoint is most of the bill.
I built a WebSocket service on API Gateway and Lambda, with Redis holding the connection map. It handled live analytics, one-to-one notifications, forced logout, and a lock that stopped two admins from editing the same record.
The service worked. I was wrong about which part of the bill mattered.
Where the connection state lives
A WebSocket is stateful and a Lambda function isn't, so most of the design is about where to keep the state between messages.
API Gateway holds the socket. The Lambda runs only for the milliseconds it takes to handle one message, and the next message may land on a different instance. So the mapping from "who is this" to "which connection do I write to" has to be stored outside the function.
I kept it in Redis, under a composite key of user, user type, status and email. On top of that sit sixteen routes: subscribe and publish, plus routes for specific events such as logout, locking a record for editing and sending a notification.
Nothing in that design is unusual.
What I expected
API Gateway bills a WebSocket API on two meters:
| Rate | |
|---|---|
| Messages, either direction | $1.00 per million (32 KB increments) |
| Connection minutes | $0.25 per million |
A connection minute accrues for every minute a socket is open, whether or not anything is sent. Most articles on serverless WebSocket costs focus on this charge, and I assumed it would dominate the bill: you pay for presence, not for work.
Then I built the model, and in most scenarios something else cost more.
The model and its assumptions
I modeled eighteen scenarios: six user counts at three message intensities.
The assumptions are listed here so you can replace them with your own. Prices are US East list rates from mid-2026, with no free tier on API Gateway. Users are connected eight hours a day, thirty days a month. Messages average 2 KB, so one message is one billable unit. Half of all messages are server pushes, so Lambda invocations are half the message count. Lambda runs at 128 MB and 50 ms, with its permanent free tier applied. Each invocation writes one kilobyte of log at $0.50/GB, and data transfer out costs $0.09/GB.
Most calculators leave out two fixed costs, and below a thousand users they matter more than anything else.
One is Redis. A cache.t4g.micro on Valkey costs $9.34 a month, and it runs whether anyone connects or not.
The other is a VPC endpoint. A Lambda that reaches ElastiCache has to run in a VPC, and a Lambda in a VPC cannot call the API Gateway Management API to push a message unless it has a route out. An interface endpoint costs about $7.30 a month in one availability zone. A NAT Gateway would cost $32.
Together that is $16.64 of fixed cost before the first connection.
Light usage: 20 messages per user per day
| Users | Fixed | Usage | Total | Fixed share |
|---|---|---|---|---|
| 10 | $16.64 | $0.04 | $16.69 | 100% |
| 100 | $16.64 | $0.45 | $17.09 | 97% |
| 1,000 | $16.64 | $4.46 | $21.11 | 79% |
| 5,000 | $16.64 | $22.42 | $39.06 | 43% |
| 10,000 | $25.99 | $45.04 | $71.03 | 37% |
| 50,000 | $44.68 | $226.01 | $270.69 | 17% |
Moderate usage: 200 messages per user per day
| Users | Fixed | Usage | Total | Connections as share of usage |
|---|---|---|---|---|
| 10 | $16.64 | $0.12 | $16.77 | 29% |
| 100 | $16.64 | $1.22 | $17.87 | 29% |
| 1,000 | $16.64 | $12.64 | $29.29 | 28% |
| 5,000 | $16.64 | $64.01 | $80.65 | 28% |
| 10,000 | $25.99 | $128.22 | $154.21 | 28% |
| 50,000 | $44.68 | $650.85 | $695.53 | 28% |
Heavy usage: 2,000 messages per user per day
| Users | Fixed | Usage | Total | Connections as share of usage |
|---|---|---|---|---|
| 100 | $16.64 | $9.40 | $26.05 | 4% |
| 1,000 | $16.64 | $95.82 | $112.46 | 4% |
| 10,000 | $25.99 | $984.58 | $1,010.56 | 4% |
| 50,000 | $44.68 | $4,950.34 | $4,995.02 | 4% |
What the model showed
1. Below a thousand users, the fixed costs are most of the bill
At light usage and ten users the invoice is $16.69, and $16.64 of it is Redis and a VPC endpoint. Usage comes to four cents.
At a hundred users the bill is $17.09, and at a thousand it is $21.11. The user count grew a hundredfold and the bill moved by about four dollars, because what you pay for is mostly rent on two components that run regardless of traffic.
This was the result that surprised me, because it inverts the usual advice. On a small serverless WebSocket service, optimizing message volume is pointless: at a hundred users and light usage you'd be tuning 3% of the invoice. The only way to lower that bill is to take out a fixed component, by using a cheaper connection store or none at all.
2. Connection minutes dominate in only three of eighteen scenarios
I expected them to dominate, and they do only at light usage from five thousand users up. At light usage connection minutes are 80% of the usage charges, and from five thousand users up those charges are most of the bill. At moderate usage connection minutes are 28% of usage, and at heavy usage 4%.
The reason is arithmetic I should have done first. A connection minute costs a quarter of what a message costs, and a user connected for eight hours generates 480 connection minutes a day, so it takes 120 messages a day for messages to draw level. Above roughly 15 messages an hour per user, messages are the bigger meter.
3. At moderate usage, a plain server is already cheaper at a thousand users
Both architectures need Redis, so the difference between them comes from everything else.
| Users | Serverless, moderate | One server | HA pair + load balancer |
|---|---|---|---|
| 100 | $17.87 | $21.72 | $50.41 |
| 1,000 | $29.29 | $22.71 | $51.40 |
| 5,000 | $80.65 | $27.14 | $55.83 |
| 10,000 | $154.21 | $42.01 | $70.70 |
| 50,000 | $695.53 | $141.73 | $207.21 |
At moderate usage and a thousand users, serverless already costs more than a single instance, and by five thousand it costs more than a properly redundant pair. At fifty thousand users it costs three to five times as much.
The figure I'd seen quoted for the crossover, and had also gotten wrong myself, is roughly a thousand always-on connections. That number comes from comparing connection minutes against a bare instance. It ignores that both sides need a connection store, that serverless also needs a route out of the VPC, and that at realistic message rates messages cost more than connections. With those included, serverless overtakes a single server before a thousand users.
The calculator below runs the same model on your own numbers. It offers the six user counts from the tables, because those are the only points where I sized the Redis node and the server.
$29.29a month
- Redis and VPC endpoint (fixed)$16.64
- Connection minutes$3.60
- Messages$6.00
- Lambda$0.40
- Logs (CloudWatch)$1.54
- Data transfer out$1.11
- Serverless$29.29
- One server$22.71
- HA pair + load balancer$51.40
Cheapest at these numbers: One server.
Fixed costs are 57% of the serverless bill, and connection minutes 28% of usage.
The same assumptions as the post: 30 days a month, 2 KB messages, half of them server pushes, Lambda at 128 MB and 50 ms with its free tier, 1 KB of log per invocation. Both server options include the same Redis and data transfer.
Operations, and the cost of logging
Operations aren't on either invoice. The serverless option comes with no patching, no scaling, no capacity planning and no on-call for a process that died. A single server is a single point of failure, which is why I also priced the redundant pair, and even the pair is two instances someone has to own.
At a hundred users, paying $17 instead of $22 so you never have to think about a server is an easy call. At fifty thousand users, serverless costs more than three times as much as the redundant pair, and someone should decide explicitly that avoiding two servers is worth the difference.
Log ingestion can also cost more than the connection minutes. At moderate usage and 10,000 users, CloudWatch is $15 against $36 of connection minutes, and verbose logging on a chatty service reverses that. Logging is the cheapest line on the bill to fix and the easiest to forget.
The client owns the connection
Two numbers look as if they need to agree: a fifteen-minute TTL on the connection record and a ten-minute refresh on the client.
For a while I treated that as a hidden coupling: two settings in two repositories, with correctness depending on the relationship between them. It works better to ask who owns the connection.
The client creates the connection, so the client owns its lifetime. It is the only side that knows whether the socket is still there, and everything on the server is a safety net for records the client never got to clean up.
That replaces an invariant you have to remember with a rule that fits in one line: the client is authoritative, and the server cleans up after it. It is also why the client's refresh has to be shorter than the record's TTL: the client owns the connection, so it has to renew the record before the server's cleanup would remove it.
Two limits that can't be raised
The idle timeout is 10 minutes: a socket with no traffic is closed. The maximum connection duration is 2 hours: every connection is closed at two hours, however active it is. Neither limit can be raised.
A ten-minute client refresh sits exactly on the idle timeout, so in practice the socket sometimes closes first. That is fine if the client refreshes by reconnecting, because it comes back either way and the record is written fresh. It would be a problem if the ten minutes were a heartbeat meant to keep the socket alive, since a keepalive has to run comfortably below the limit.
The two-hour limit also means a dashboard left open all day reconnects at least twelve times, so the design can't rely on a long-lived connection.
Stale connection records cost money
A connection record that outlives its socket makes someone look online when they aren't, and it also makes the server send to them. The write goes out and API Gateway bills it as a message, but nobody receives it.
On a service that pushes notifications and live updates to a list of users who are supposed to be connected, you pay for every message sent to someone who has left, and the cost grows with the number of stale records. It also goes unnoticed, because a message to a dead connection produces no error that anyone watches.
The model puts a number on it. At moderate usage half the messages are server pushes, so at 10,000 users that is thirty million pushes a month. If 10% of connection records are stale, three million of those messages are billed and never received: $3 a month. At 30% stale it is $9, and at fifty thousand users with 30% stale, $45.
Those amounts are small, but they are pure waste. They grow with the stale set rather than with your traffic, and this is the one line on the invoice where you know for certain you got nothing for the money.
When a push fails, API Gateway returns 410 Gone. If the handler deletes the record as soon as it gets that response, it cleans up the presence state and stops the spending in the same step, with no coordination with the client.
Each of the three mechanisms covers a different case:
| Handles | |
|---|---|
| Client refresh | the connection it owns, while it's alive |
410 Gone on push | records that died between two messages |
| TTL | records nothing ever touched again |
None of the three depends on a number in someone else's repository.
Generating the route config from the code
One decision from that service I would make again: I stopped writing the deployment config by hand.
Serverless configuration has to declare every route. With sixteen routes, the config is a file you edit carefully and still get wrong. A route that exists in the code but has no entry in the config doesn't exist at all, and nothing warns you. So the config is generated from the code, and the two can't drift apart because there is only one source. Sixteen routes are already more than a person should reconcile by hand.
All figures use US East list rates as of mid-2026, assume no API Gateway free tier and apply Lambda's permanent free tier. They exclude load balancer LCU charges, cross-AZ transfer, backups and reserved pricing, all of which would move the persistent-server columns, most of them upward. The model is arithmetic on published rates, and I did not measure a running system.
Writing
If you’re working on something similar, send us the details.
The first call takes 30 minutes, and you’re under no obligation.