# A live auction on Durable Objects

> How do thousands of bidders share one live auction without races, and what happens when it ends? One Durable Object per lot holds every WebSocket and processes bids one at a time. When the lot closes, it reconciles into AWS and deletes its data. $15–30 a month vs $130–410 on AWS designs.

- A decision guide from The Stack by Nicolas Dao (Cloudless Labs, Sydney)
- Web page: https://cloudlesslabs.com/stack/guides/live-auction/. The whole stack in one file: https://cloudlesslabs.com/stack/brief.md
- Tools: [Cloudflare Durable Objects](https://cloudlesslabs.com/stack/cloudflare-durable-objects/index.md) (Use when it fits), [Cloudflare Workers](https://cloudlesslabs.com/stack/cloudflare-workers/index.md) (My default), [Neon (serverless Postgres)](https://cloudlesslabs.com/stack/neon/index.md) (My default), [Amazon S3](https://cloudlesslabs.com/stack/aws-s3/index.md) (My default)

## The one idea

`idFromName("lot-123")` reaches the same single instance anywhere in the world. Everyone watching lot 123 connects to that one object, which handles one event at a time. Two bids in the same millisecond are processed in turn; the second sees the first's result.

## The protocols

| Protocol | Role in the auction |
|---|---|
| WebSockets (hibernating) | Every watcher's live connection. Idle objects sleep and aren't billed; outgoing pushes are free. |
| HTTP | Page load: current price, leader, time left. |
| RPC from a Worker | `stub.placeBid(user, amount)`: the REST bid endpoint and admin actions. |
| Alarms | One timer per object at the close time. Anti-sniping just moves it. |
| SQLite | `lot` and `bids` tables inside the object. |
| Outbound fetch | Sends the result to AWS. |

## The flow

```
 Browsers (500–10,000 per lot)
      │ wss://auctions.example.com/lot/123
      ▼
 Worker ── verify JWT, rate-limit, route by lot ID
      │ idFromName("lot-123")
      ▼
 LotDO "lot-123" (one instance worldwide)
   SQLite: lot + bids · holds every socket · alarm at close time
   │                               │
   │ broadcast each new price      │ alarm fires → lot SOLD
   ▼ (in order, free)              ▼
 every watcher          POST result + full bid log to AWS
                        (retries by alarm until AWS acknowledges)
                                   ▼
          Function URL → SQS → Lambda → Postgres (invoice, Stripe, reports)
                                      └→ S3 Parquet (data lake)
                                   ▼
                        object deletes its own data
```

The Worker is the door, the object is the room, and AWS gets the minutes when the meeting ends.

## The gotcha: await

Single-threaded doesn't mean transactional across an `await`. If the object awaits an outside call (asking AWS whether the bidder is verified) between reading the price and writing the bid, other bids run in the gap. Do outside checks first; keep read → check → write → broadcast synchronous.

Also: every deploy restarts all objects and drops every socket. Clients reconnect automatically; don't deploy during a live auction.

## Cost: 2,000 lots a month, 500 watchers each, 300M pushed updates

| Design | Monthly |
|---|---|
| Durable Objects + Worker | $15–30 |
| Self-run Fargate + load balancer + Valkey | ~$130–165, plus deploys, scaling and failover |
| AppSync Events + Lambda | ~$310 |
| API Gateway WebSocket + Lambda + DynamoDB | ~$407 ($390 of it is pushes) |

Postgres takes the bids fine with a conditional UPDATE. What's hard on AWS is holding thousands of sockets and pushing every update in order: one hot lot with 10,000 watchers needs 40,000 pushes a second, four times API Gateway's default account limit.

## Durable Objects beat everything else when all four are true

1. One thing with shared live state (a lot, room, document, game, device).
2. Many clients connected to it at once, receiving pushes.
3. A single referee: first wins, no double-booking.
4. A lifespan or independence: it lives for an event, and you rarely query across many things.

## Related

- [Something live that many people watch at once](https://cloudlesslabs.com/stack/use-cases/realtime.md): the use case this guide works through
- [Cloudflare Durable Objects](https://cloudlesslabs.com/stack/cloudflare-durable-objects/index.md): the full entry (verdict, pricing, gotchas)
- [Cloudflare Workers](https://cloudlesslabs.com/stack/cloudflare-workers/index.md): the full entry (verdict, pricing, gotchas)
- [Neon (serverless Postgres)](https://cloudlesslabs.com/stack/neon/index.md): the full entry (verdict, pricing, gotchas)
- [Amazon S3](https://cloudlesslabs.com/stack/aws-s3/index.md): the full entry (verdict, pricing, gotchas)
- [Every decision guide](https://cloudlesslabs.com/stack/brief.md): listed at the end of the brief
- [Map of every Markdown file on cloudlesslabs.com, with what each one answers](https://ask.cloudlesslabs.com)
