AWS Builder Center
We almost got bitten by a snake, yet the hard part was streaming the heart rate.

We almost got bitten by a snake, yet the hard part was streaming the heart rate.

We wanted a smartwatch to warn about heat strain before someone collapses. The cloud wasn't the hard part. Getting a heart rate off the wrist, every five seconds, was.

At dusk on 11 October 2026, just a few hours before the submission, we were on a lawn at IIT Kharagpur, filming the outdoor part of our demo. A passer-by stopped and warned us that a snake was about five metres away.

So yes, this hackathon nearly got us bitten by a snake. It still wasn't the hardest part of the weekend. That was a number every smartwatch already measures: your heart rate. We needed it on our own server, live, every few seconds. We couldn't find an app that would send it there, so we wrote one.
The short version* WristHeat reads each person's own smartwatch, estimates their heat strain on AWS, and shows GREEN, AMBER or RED to the wearer and to whoever is responsible for them.
  • The hardest problem: we found no app that streams live heart rate from a Galaxy Watch to a server we control. We nearly dropped the idea, then built our own Wear OS app.
  • On AWS: API Gateway, Lambda, DynamoDB, Step Functions, EventBridge Scheduler, SNS, Polly, Cognito, Secrets Manager and CloudWatch, all in one CDK stack in Mumbai ( ap-south-1 ).
  • Demo day: a real run turned AMBER on the watch, and the data confirmed recovery 7 minutes later. For a simulated trainee, Step Functions escalated after 90 s with no answer, and kept the incident open until the data showed recovery.
  • It is not a medical device. The core temperature is an estimated trend, not a thermometer.
We built it for AWS Environmental Hacks 2026 (WeMakeDevs, Heat & Water track). The live site, code and video are linked at the end.

A city gets one number. Heatstroke happens to one person.

About 489,000 people die from heat in an average year ( WHO , 2000–2019 average). The cases that stayed with us were people exerting with someone in charge nearby:
Who
What happened
Source
Recruitment candidates
12 died during an excise constable recruitment run in Jharkhand, 2024
Deccan Herald
School children
23 students fainted after a morning assembly in Chitrakoot, UP, August 2024
Careers360
Workers
2.41 billion workers exposed to excessive heat; about 19,000 deaths and 22.85 million injuries a year
ILO, 2024
Pilgrims
1,301 deaths during Hajj 2024
Hajj 2024 summary
India, summer 2024
40,984 suspected heatstroke cases and 114 deaths, 1 March to 18 June (NCDC)
South First
A heat warning gives everyone in a place the same number. The runner pushing on, or the candidate in the eighth kilometre, can't tell when their own body is at its limit. Neither can the recruiter, PE teacher or coach watching twenty of them. The warning says it is hot. It doesn't say who to stop.

What we built

WristHeat combines two signals. The first is the body: heart rate from each wearer's own watch, turned into an estimated core-temperature trend and a strain index. The second is the place: the site's heat flag, computed from a weather forecast.

On your own
With a group
For
A runner, hiker or lone worker
A coach, PE teacher, drill instructor or site supervisor
Sees
Their own page (/me)
A console with the whole group, a Needs action now list in plain words, and an activity feed
On RED
The watch buzzes; a button on /me confirms "I've stopped and I'm cooling down"
The supervisor gets an email with a spoken alert in English and Hindi
No answer in 90 s
Their emergency contact is emailed
A backup contact is emailed
Incident closes
When the data confirms recovery
When the data confirms recovery
The WristHeat landing page: a group console with four cards and an orange SLOW DOWN watch face The landing page. The four group cards are simulated trainees, and each is labelled SIMULATED.

The hardest problem: a heart rate nobody would hand over

On paper, the data was the easy part. The watch measures heart rate, Samsung Health shows it, and Health Connect stores it. We assumed some app would pass it on live.
On Saturday night we tested every route we could think of. Our notes from that night list them:
  • Bluetooth. Many sports watches broadcast the standard Bluetooth heart-rate profile, which a web page can read with Web Bluetooth. Brave, our usual browser, blocks Web Bluetooth. In Chrome, our Galaxy Watch5 Pro didn't advertise the profile at all.
  • Health Connect. We checked whether Samsung Health had synced heart rate into Health Connect for another app to read. It showed no heart-rate records.
  • Relay apps. The ones we found send heart rate to their own service, not to a server we control.
  • Exports. Samsung Health and Health Connect can export data. An export after the run can't warn anyone during it.
Our go/no-go plan had a gate for this. If the watch couldn't stream, WristHeat was out and we would build a fallback idea. Every route failed. At 00:02 on Sunday 11 October we decided to keep the idea and take the heart rate from an app running on the watch. The first commit of that app landed at 01:21.
Diagram of the watch app: Health Services ExerciseClient, SampleBuffer, Uploader every 5 s and NetworkKeeper, then API Gateway and Lambda, with state and alerts coming back The pipe we had to build. The green lines are fixes that only real use exposed.
The app is Kotlin, running as a foreground service of type  health . Health Services'  ExerciseClient  delivers heart rate about once a second. Samples go into a small in-memory buffer. An uploader sends them to API Gateway every 5 seconds and backs off when the network fails. Every response carries the current state and, when the state changes, a one-shot alert that makes the watch buzz. 34 unit tests cover the API client, the buffer, the uploader, the alert policy and the session state. Three things fought back.

1. The screen went dark, and so did the data

With the screen off, Health Services batched heart rate and delivered it in bursts several minutes apart. A heat warning that arrives minutes late is useless. The fix was a batching override that this watch supports:
Plain text
// With the screen off Health Services batches HR for minutes; ask for 5-s delivery.val batching = setOf(BatchingMode.HEART_RATE_5_SECONDS) .filter { it in caps.supportedBatchingModeOverrides }.toSet()val config = ExerciseConfig.builder(ExerciseType.RUNNING) .setDataTypes(setOf(DataType.HEART_RATE_BPM)) .setIsAutoPauseAndResumeEnabled(false) .setIsGpsEnabled(false) .setBatchingModeOverrides(batching) .build()
We checked it on the wrist. With the app in the background, 150 s gave 30 uploads and 141 samples, with no gap longer than 5 s. With the screen dozing, 90 s gave 18 uploads and 86 samples.

2. A ghost "exercise ended"

If our app crashed or was force-stopped, its old exercise kept running. When a new session started, that leftover exercise's late "ended" update arrived on the new callback. The app read it as its own session ending and stopped. The fix was to compare the update's start time with the moment we asked to start:
Plain text
// Updates from an older exercise (a leftover we superseded) can arrive on this// callback; only an end of the exercise we started means tracking stopped.val since = requestedAtval ours = since != null && update.startTime?.isBefore(since.minusSeconds(2)) == falseif (state.isEnded) { if (ours && !stopRequested) onError("exercise_ended")}
The  stopRequested  check handles the same problem from the other side. Pressing our own Stop button also produces an "ended" update, and that one isn't an error either.

3. Demo day: a watch with no internet

Our Galaxy Watch5 Pro has Wi-Fi and Bluetooth, but no LTE. On the first outdoor run, away from Wi-Fi, the watch lost its internet connection. The app did what we built it to do: it kept the samples and retried. But the minute that turned AMBER reached AWS about 8 minutes late. A late warning like that is no use to anyone.
We fixed it in two steps:
  • For the second run, we put the watch on a phone hotspot. Every minute then reached AWS within 2–9 seconds.
  • In the app (commit  5dc314b ), the session now asks Android for a Wi-Fi network and holds it. Wear OS turns Wi-Fi off unless an app asks for it. We also found that stopping a session used to throw away anything still buffered, so the app now flushes the buffer before it ends:
Plain text
// NetworkKeeper.start(): hold Wi-Fi (home or phone hotspot) for the sessionval request = NetworkRequest.Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .build()runCatching { cm.requestNetwork(request, callback) }
// SessionService.stopSession(): send what was buffered while offline before endinguploaderRef?.let { up -> withTimeoutOrNull(8_000) { up.flush() } }
The second run used the hotspot with the earlier build. The hold-Wi-Fi build passes its unit tests but hasn't run a full session on the watch yet; that's on our list below.

From heartbeats to GREEN, AMBER and RED

The risk engine is pure Python with no AWS imports, so we test it offline against published reference values. It works one minute at a time:
  1. Average the minute's heart rate. A minute with fewer than 10 samples counts as a gap.
  1. Calibrate for 2 minutes. The wearer sits still, and the lower of the two minute averages becomes their resting heart rate.
  1. Update the core-temperature estimate with ECTemp, a Kalman filter from the US Army Research Institute of Environmental Medicine (Buller et al., 2013). It turns each minute's heart rate into an estimated core-temperature trend.
  1. Compute the strain index and a candidate level:
Plain text
def psi(ct: float, hr: float, hr0: int) -> float: """Moran 1998 PSI, unrounded, clamped to [0, 10].""" value = 5 * (ct - 37.1) / (39.5 - 37.1) + 5 * (hr - hr0) / (180 - hr0) return min(10.0, max(0.0, value))
def candidate_level(psi_value: float, ct: float, flag: str) -> Literal["GREEN", "AMBER", "RED"]: hot = flag in ("RED", "BLACK") warm = flag in ("YELLOW", "RED", "BLACK") if psi_value >= 7.0 or ct >= 39.0 or (psi_value >= 6.0 and hot): return "RED" if psi_value >= 5.0 or (psi_value >= 4.0 and warm): return "AMBER" return "GREEN"
  1. Change state only on evidence. A higher level must hold for two consecutive minutes before the state rises. Dropping back gives RECOVERING, without an alert. GREEN returns only after three good minutes: GREEN level, heart rate within 30 bpm of resting, strain index below 4, and a core trend that isn't rising.
The watch during calibration: "Sit still, 0:57, 90 bpm, measuring your resting heart rate" The first two minutes of every session: sit still while the watch learns your resting heart rate.
The heat flag comes from the other side. Every 10 minutes, EventBridge Scheduler runs a Lambda that fetches the forecast from Open-Meteo, computes wet-bulb globe temperature (WBGT) with the Liljegren method, and maps it to a US Army TB MED 507 flag. If that data is more than 45 minutes old, the engine falls back to the body signal alone.

The escalation that waits for the body

RED opens an incident and starts a Step Functions execution. It is our favourite part of the AWS design because of task tokens. The workflow pauses twice, and each pause is released by something different:
  • WaitForAck is released by a person. When the supervisor taps Acknowledge in the console, the supervisor-API Lambda calls  SendTaskSuccess . If 90 seconds pass first, the task times out and the workflow escalates to the backup contact.
  • WaitForRecovery is released by the data. The ingest Lambda sends that token only when the engine emits  RECOVERY_CONFIRMED .
Plain text
const wait = (id: string, kind: string, seconds: number) => new tasks.LambdaInvoke(this, id, { lambdaFunction: registerFn, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ incident_id: sfn.JsonPath.stringAt('$.incident_id'), kind, token: sfn.JsonPath.taskToken, }), taskTimeout: sfn.Timeout.duration(Duration.seconds(seconds)), resultPath: sfn.JsonPath.DISCARD,});
const waitForAck = wait('WaitForAck', 'ack', props.ackTimeoutSeconds); // 90 sconst waitForRecovery = wait('WaitForRecovery', 'recovery', 3600);waitForAck.addCatch(escalate, { errors: ['States.Timeout'], resultPath: '$.timeout' });
That separation is the point of the product: an incident can be acknowledged and still not be over. Nobody has to poll, and no button closes an incident. If an hour passes without confirmed recovery, the workflow closes it anyway, with the outcome recorded as "not confirmed".
AWS Step Functions graph view: Notify, WaitForAck with a caught timeout, then Escalate, WaitForAckAfterEscalation, WaitForRecovery and Close, all succeeded The live execution for the group demo. WaitForAck shows a caught error: nobody answered in 90 s, so the workflow escalated.
The Notify step uses Amazon Polly (the neural Kajal voice) to render the alert in English and Hindi, saves the audio to S3, and emails the supervisor through SNS. Here is how the run went, with one simulated trainee, Kabir, in a group of four:
Step
Time (IST)
After RED
RED opened
18:14:05
—
Supervisor emailed
18:14:06
1 s
No answer, escalated to backup
18:15:38
93 s
Acknowledged
18:17:43
3 min 38 s
Recovery confirmed by the data, incident closed
18:23:04
8 min 58 s
Kabir spent 7 minutes in RED, with a peak strain index of 8.2 and a peak estimated core of 38.61 °C.
The closed incident for Kabir (SIMULATED): time to acknowledge 3 min 38 s, time to confirmed recovery 8 min 58 s, and a timeline The closed incident on the live system. Note "Template summary (Bedrock unavailable)" under the report. More on that below.

Everything else on AWS

Architecture: Galaxy Watch, simulator and browser into API Gateway; Lambdas for ingest, supervisor API, web and context; DynamoDB; Step Functions; SNS, Polly, Bedrock; EventBridge Scheduler; Cognito; Secrets Manager; CloudWatch One AWS CDK stack (TypeScript) in  ap-south-1 : 8 Lambda functions, 6 DynamoDB tables, 1 state machine, 2 SNS topics.
Service
Its job in WristHeat
API Gateway (HTTP API)
Receives the watch stream, protects supervisor routes with JWTs, serves the dashboard
Lambda (Python 3.12, arm64)
Risk engine, supervisor API, heat and air fetch, workflow steps, web
DynamoDB (on-demand, TTL)
Sites, trainees, sessions, readings, per-minute estimates, incidents
Step Functions
Notify → wait for ack → escalate → wait for confirmed recovery → close
EventBridge Scheduler
Refreshes the site's heat flag and air quality every 10 minutes
SNS and Polly
Email with an English and Hindi spoken alert
Cognito
Two groups: supervisors see the group; wearers see only their own data and get a 403 on every group route
Secrets Manager
The device token the watch sends with every upload
CloudWatch, Powertools, X-Ray
A WristHeat dashboard, custom metrics and traces
Over 12 hours, CloudWatch counted 62,718 heart-rate readings ingested (most from the simulator), with a median of 24 ms to process a batch (p95: 39 ms).

A brand-new AWS account fought back too

Our account was new, and that showed. Until the afternoon of 11 October, calls to several services returned  SubscriptionRequired . CloudFront stayed blocked pending account verification, so API Gateway and a small Lambda serve the dashboard over HTTPS. The CloudFront path is still in the stack, behind a CDK context flag. Bedrock (Nova Lite) was blocked too. Its only job is to word the incident report from logged facts, so the report falls back to a deterministic template and says so on screen. We'd rather show a labelled template than hide the gap.

Demo day, in numbers

The watch: an orange SLOW DOWN alert at 5:46, then a green "Recovery confirmed" at 5:53 Varad's live run: AMBER at 17:46, recovery confirmed at 17:53.
On your own (Varad, real run, phone hotspot)

AMBER on the watch
17:46, after 2 minutes above 130 bpm (a demo threshold, labelled on screen)
Recovery confirmed by the data
17:53, 7 minutes later
Each minute reached AWS
within 2–9 s
Tests
Count
Backend (engine, API, escalation)
149
Simulator
11
Dashboard
51
Infrastructure (CDK assertions)
12
Watch app (unit)
34

Three things we'd tell ourselves on day one

  1. Prove the data path first. The idea lived or died on one heart rate reaching one server. We made that a go/no-go gate, and it was the right gate.
  1. Test on the network you will actually have. On home Wi-Fi, everything worked. Outside, the watch had no internet, and the AMBER arrived 8 minutes late.
  1. Acknowledged is not recovered. Make the system wait for the data, and label anything simulated, everywhere.

What's next

  • Compare the wrist estimate against a chest strap and, where possible, a measured core temperature, before trusting it further.
  • Field-test the hold-Wi-Fi build over a full session and measure the battery cost, which we haven't measured yet.
  • Switch on CloudFront and Bedrock once account verification completes.
  • Per-device tokens instead of one shared token, more than one site per deployment, and more Wear OS watches.

Honest limits

  • Not a medical device. WristHeat estimates a core-temperature trend from heart rate. It isn't a thermometer.
  • ECTemp was validated with chest-strap heart rate (about ±0.6 °C), not with wrist optical sensors like this watch's. Studies found it misses many cases above 39 °C. Treat it as an early indicator, never as clearance to continue.
  • WBGT is for open ground in sun, from forecast data. Shade, indoors and local conditions differ.
  • The rules combining strain with the heat flag are our own heuristic, not a published standard.
  • The group demo used simulated trainees, labelled SIMULATED on every screen. The 130 bpm AMBER trigger in the solo run was a demo setting.
  • We tested on one watch, a Galaxy Watch5 Pro without LTE. It needs Wi-Fi or a phone hotspot.
  • ECTemp is patented (US 10702165), so this is a non-commercial research prototype. We don't claim any prevented illness or measured impact.

Links, AI tools and credits

  • Demo video: [YOUTUBE LINK — to be added]
AI tools. We built WristHeat with Claude Code (Anthropic): research, design, the AWS backend, the dashboard, the simulator, the tests and the Wear OS app. It also helped draft this post. At runtime, Amazon Bedrock (Nova Lite) only words the incident report from logged facts, with a template fallback.
Credits. Weather data by  Open-Meteo.com  (CC BY 4.0); air quality from the Copernicus Atmosphere Monitoring Service via Open-Meteo. ECTemp is from the US Army Research Institute of Environmental Medicine; the work/rest guidance is US Army TB MED 507.
Built for AWS Environmental Hacks 2026, part of the WeMakeDevs × AWS Builder Center Bharat Builds Tour.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article