There are roughly 10 million people with Type 1 diabetes worldwide, with half a million new cases diagnosed every year. Nightscout, the open-source continuous glucose monitoring (CGM) platform, has been forked more than 70,000 times on GitHub. It's a lifeline for patients and caregivers who need real-time access to glucose data on their phones, watches, and browsers.
Yet most deployments still run on container platforms with MongoDB, a stack that made sense when Heroku was free but feels today increasingly heavy and expensive. When platform after platform dropped their free tiers, the community scrambled to find alternatives. Railway, Northflank, Fly.io. But never AWS serverless. It was about time someone changed that.
The Starting Point
A close family member has Type 1 diabetes and uses a Freestyle Libre continuous glucose monitor. Like many in the CGM community, we relied on Nightscout, the open-source project that lets you view glucose data remotely, share it with caregivers, and connect apps like xDrip and Shuggah on Apple Watch.
The classic Nightscout setup used to run free on Heroku with MongoDB Atlas. When Heroku killed their free tier, I moved to Northflank, which worked for a while, until it didn't. The free container IP addresses got banned by Abbott's servers for some reason (probably too many Nightscout users on the same shared IPs), and I ended up paying $15/month for a dedicated container just to keep the data flowing. Fifteen dollars a month to relay one glucose reading per minute felt absurd.
I wanted something I fully controlled, something minimal, and something cheap.
So I built my own serverless Nightscout on AWS from scratch.
The Catalyst
I'd had the idea of building this on AWS serverless for a long time. The architecture was clear in my head: Lambda, DynamoDB, API Gateway, done. But between work and life, I never found the time to actually sit down and build it. Writing Terraform modules, implementing Lambda handlers, figuring out the LibreLink Up API, wiring it all together. It was a project that always stayed in the "someday" pile.
Then Kiro came along. With an AI development environment that could help me scaffold infrastructure, implement the Lambda functions, debug issues, and iterate fast, that "someday" project suddenly became a "this weekend" project.
The Idea
The concept was simple: replace the entire Nightscout stack with AWS serverless services. No servers to manage, no MongoDB to babysit, pay-per-use pricing. For a single patient system that processes one glucose reading per minute, the theoretical cost should be pocket change.
The architecture:
- EventBridge Scheduler triggers a Lambda function every minute
- The Lambda fetches data from Abbott's LibreLink Up API
- Stores it in DynamoDB
- Another Lambda serves a Nightscout-compatible REST API via API Gateway
- A static web app on S3 + CloudFront shows the glucose graph
- Cognito handles authentication for the web UI
All defined in Terraform. All serverless. All mine.
Building It
The build started with Kiro's spec-driven development. I wrote a couple of lines describing what I wanted: a serverless Nightscout on AWS that fetches data from LibreLink Up and serves it via a compatible API. I also pointed Kiro at two open-source projects as reference material: cgm-remote-monitor (the original Nightscout) to understand the API contract and data models, and nightscout-librelink-up to understand how the LibreLink Up data fetching works. Kiro studied both, then produced a full spec: user stories with acceptance criteria, a technical design document covering architecture decisions and data models, and a structured list of implementation tasks. Then it started executing them, one by one.

One of the user stories Kiro generated from my two-line prompt. Complete with acceptance criteria and technical context.

The high-level architecture diagram from Kiro's design document. This is what got built.
Phase 1: Infrastructure
I started with Terraform modules: DynamoDB tables, Lambda functions, API Gateway, Secrets Manager. Modular, reusable, properly parameterized. The kind of infrastructure code that makes you feel responsible and adult.
Phase 2: The CGM Fetcher
The core Lambda function authenticates with LibreLink Up, fetches glucose data, and writes it to DynamoDB. Go was the obvious choice: fast cold starts, tiny binaries, and it compiles to a single static binary that just works on Lambda.
Phase 3: The API Handler
A Nightscout-compatible API that serves data to existing clients. The beauty of Nightscout is its ecosystem. Dozens of apps already speak its protocol. I just needed to implement the endpoints and existing apps would work out of the box.
Phase 4: The Web App
Vanilla HTML/CSS/JavaScript. No React, no build step, no node_modules black hole. A glucose graph, current reading, trend arrow, and time-in-range stats. Served from S3 through CloudFront for pennies.

The finished web app. Real-time glucose graph, trend arrow, time-in-range stats. Vanilla JS, no frameworks.
Phase 5: Authentication
Cognito for the web UI with Lambda@Edge enforcing auth at the CDN level. API endpoints stay open with API key auth for backward compatibility with mobile apps.
Phase 6: Data Migration
Three years of glucose data lived in the old Nightscout's MongoDB. Kiro wrote all the scripts needed for the migration: downloading the data from MongoDB, transforming it from Nightscout's MongoDB format into the DynamoDB schema, validating each record, and uploading it in small chunks to avoid throttling the table. Roughly 1.5 million readings moved over without drama. This is where things got interesting cost-wise, though.
The $16 Problem
After everything was running, I checked the bill. About $10/month. For a system that processes one reading per minute. That seemed... wrong.
DynamoDB was the culprit, eating more than $8 of that. But was it storage or operations?
I had 3 years of data: 1.5 million items across the base table and two Global Secondary Indexes. At $0.25/GB, storage was only about $0.60/month. So the cost was overwhelmingly from read and write operations.
The write spike from the migration made sense: 7.9 million write request units in March. Migrating 1.5M items through two GSIs with conditional writes (many failing because items already existed, but DynamoDB charges for those too). One-time cost, acceptable.
But the ongoing costs were still ~$16/month projected. Time to dig deeper.
The 720 Writes Per Minute Problem
Here's where I discovered something beautifully stupid.
The CGM fetcher runs every minute. LibreLink Up's /graph endpoint returns not just the current reading, but the last ~12 hours of historical readings. About 720 data points. Every single time.
My fetcher was supposed to filter out duplicates. It had a QueryLastEntry function that found the most recent entry in DynamoDB, then only wrote entries newer than that.
The problem? QueryLastEntry used a table scan with Limit: 1000.
With 1.5 million items in the table, scanning 1000 random items and picking the highest timestamp is like searching for the newest book in a library by checking the first shelf you walk past. You'll almost certainly miss the actual newest one.
So the filter thought the "latest" entry was months old. Every minute, it dutifully wrote practically all 720 historical entries. With conditional writes (attribute_not_exists) to prevent duplicates, which meant DynamoDB accepted the request, checked the condition, rejected the write, and charged me anyway. Seven hundred and twenty times. Every sixty seconds.
That's ~2,160 write request units per minute (720 × 3 for the base table plus two GSIs). Over 3 million wasted WRUs per month.
The fix was one query:
input := &dynamodb.QueryInput{
TableName: aws.String(d.tableName),
IndexName: aws.String("DeviceTimestampIndex"),
KeyConditionExpression: aws.String("#d = :device"),
ScanIndexForward: aws.Bool(false), // newest first
Limit: aws.Int32(1), // just one
}
Query the GSI in reverse order, take the first item. Correct answer, every time, for 0.5 RRUs instead of 500.
After deploying: writes dropped from ~175,000/day to ~3,000/day. A 98% reduction.
The Million Reads Mystery
Writes were fixed. But reads were still ~1 million RRUs per day. For a system with maybe 4 active clients.
I traced it to the DeviceTimestampIndex GSI: over 1 million reads per day, all from there. The API handler's QueryEntries function was the culprit.
Two Shuggah (xDrip) clients on mobile phones poll every ~16 seconds, requesting count=1. Just the latest glucose value for the watch face. Perfectly reasonable. About 10,000 requests per day.
But QueryEntries had no Limit on its DynamoDB query. It asked for the entire GSI partition (1.5 million items sorted by timestamp), DynamoDB started reading and returned up to 1 MB of data per page, and the code took the first item and threw the rest away.
Each count=1 request was reading thousands of items from DynamoDB. Ten thousand requests per day, each consuming ~100 RRUs instead of 0.5.
The fix: one line.
Limit: aws.Int32(int32(count)),
Tell DynamoDB to stop reading after you have what you need.
The Result
| Metric | Before | After |
|---|---|---|
| Writes/day | 175,000 | 3,000 |
| Reads/day | 1,000,000 | ~10,000 |
| Projected DynamoDB cost | $16/month | $0.61/month |
The entire AWS bill for a fully functional, Nightscout-compatible CGM monitoring system: about $1.50/month.
The CloudWatch Free Tier Trap
Even after fixing DynamoDB, the bill was still around $2/month. It ran fine at $0.05/day for the first three weeks of each month, then jumped to $0.09/day for the last week. Classic free tier exhaustion pattern.
The culprits:
Too many alarms. CloudWatch gives you 10 free alarms. I had 11 (7 in eu-central-1, 4 in us-east-1). Three of those were DynamoDB throttle alarms that would never fire with on-demand billing and my traffic level. Removed them.
API Gateway access logging. API Gateway logs are classified as "Vended Logs" in CloudWatch, which have a much smaller free tier (~0.21 GB) than standard log ingestion (5 GB). At ~10 MB/day, the free tier ran out around day 21. I already had Lambda-level logging that showed the same information, so I disabled the API Gateway access logs entirely.
After those two changes: 7 alarms (within the 10 free tier), zero vended log bytes. The CloudWatch line item dropped to essentially zero.
Lessons Learned
1. DynamoDB charges for work, not results. A conditional write that fails still costs a WRU. A query that returns 1 MB but you only use 1 item still costs for 1 MB of reads. DynamoDB is honest. It bills you for the I/O it performed, not the I/O you wanted.
2. Table scans are not queries. Scanning 1000 items and picking the max is O(n) and non-deterministic. Querying a GSI in reverse with Limit 1 is O(1) and always correct. Know your access patterns.
3. Always set Limit. If you need 1 item, tell DynamoDB you need 1 item. Don't ask for everything and filter in application code.
4. LibreLink Up returns history every time. The graph endpoint gives you ~12 hours of data on every call. If you don't filter properly, you'll rewrite the same data 1,440 times per day.
5. Cost Explorer is your friend. Break down costs by usage type. "DynamoDB costs $8" tells you nothing. "24 million read request units from the DeviceTimestampIndex GSI" tells you exactly where to look.
6. The free tier masks problems. If you have AWS credits or free tier, you might not notice you're burning resources until the credits run out. Check usage quantities, not just dollar amounts.
The Stack
For anyone wanting to build something similar:
- Compute: Lambda (Go, ARM64) for fast cold starts and minimal cost
- Storage: DynamoDB (on-demand) scales to zero with no fixed costs
- API: API Gateway HTTP API, cheaper than REST API
- CDN: CloudFront + S3 for a static web app at pennies
- Auth: Cognito + Lambda@Edge at $0.07/month
- IaC: Terraform for reproducible, version-controlled infrastructure
- Scheduling: EventBridge Scheduler triggers the fetcher every minute
Total infrastructure: ~$1.50/month, dominated by the $0.80 fixed cost for Secrets Manager (2 secrets at $0.40 each). DynamoDB, Lambda, API Gateway, and CloudWatch all fit comfortably in the free tier.
Was It Worth It?
I replaced a $15/month Northflank deployment with a fully serverless system I own completely, for $1.50/month. The data never leaves my AWS account. The code is mine. The infrastructure is defined in Terraform and deploys in minutes.
But the money isn't the greatest catch here.
The real takeaway is that with Kiro, you can compress the path from idea to production from weeks to hours. A project that sat in my "someday" pile for over a year was running in production over a weekend. Terraform modules, Go Lambda functions, DynamoDB schemas, API compatibility, web frontend, authentication. All of it.
That said, Kiro also writes errors and false logic from time to time. The 720-writes-per-minute bug? That came from a QueryLastEntry implementation that looked reasonable at first glance but fell apart at scale. The missing Limit on the API queries? Same story. Code that works in your head but bleeds money in production.
You need to evaluate and monitor the results. You need the hunch for what seems fine versus what smells like a problem. The $10 bill was the smell. The 1000-item scan was the false logic. Kiro gets you to production fast, but you still need to be the one who looks at the bill, checks the CloudWatch metrics, and asks "why is this number so high?"
The combination works: Kiro handles the volume of code, you handle the judgment calls. And sometimes those judgment calls are "this DynamoDB cost doesn't add up" followed by a conversation with Kiro to debug it together.
Sometimes the journey is the destination. And sometimes the destination is a $1.50 monthly bill and a Lambda function that no longer desperately tries to write 720 duplicate glucose readings every minute.
Get the Code
My project is open source and available at github.com/pmalmirae/serverless-nightscout-on-aws. I'm happy to share it with anyone who wants to run their CGM data on their own AWS account. If this helps even one person get off an expensive hosted setup and onto something they fully control, it was worth open-sourcing. Consider it my contribution back to the Nightscout community that gave us so much in the first place.
What's Next
A couple of directions I'm considering for the project:
Modular Terraform structure. Reorganizing the infrastructure into independent modules: Backend (DynamoDB + API), Web UI (S3 + CloudFront + Cognito), and a separate Libre Data Fetcher module. This would make it possible for contributors to develop data fetchers for other CGM devices (Dexcom, Medtronic, ...) without touching the rest of the stack.
Richer statistics in the Web UI. The current dashboard shows up to 24-hour glucose graph and time-in-range. The next step is adding estimated HbA1c (long-term blood glucose balance), daily and weekly trend analysis, and pattern detection. The data is already in DynamoDB, it just needs the math and the visualization.
If any of this sounds interesting to you, contributions are welcome. Whether it's a data fetcher for another CGM device, improvements to the web dashboard, documentation, or just testing it on your own account and reporting what breaks. Open an issue, send a PR, or just star the repo if you want to follow along. Let's build something useful together.
Top comments (0)