AWS Builder Center

Why we refused to build heat surge pay: a heat-safe shift planner on AWS

HeatShift turns tomorrow's forecast into a heat-safe shift plan for delivery riders and outdoor crews: stop windows in extreme heat, paid breaks, cool rest points and a hard block at dispatch. On a real Gurugram heatwave it cut dangerous worker-hours from 732 to 9. Here's why we chose Step Functions, EventBridge, DynamoDB and Amazon Location, and what broke along the way.

13:00, Cyber City, 28 May 2026

At 1 pm on 28 May 2026, the estimated Wet Bulb Globe Temperature (WBGT) in Gurugram's Cyber City reached 33.2 °C. Most people have never heard of WBGT, but occupational safety uses it to decide whether people should be doing hard physical work outside. Above about 31.5 °C, the guidance for moderate work is simple: stop. At that point the body can no longer shed the heat it produces. Sweat doesn't evaporate quickly enough, and core temperature keeps climbing.
That afternoon, delivery riders were still out on the roads in Cyber City, MG Road and Golf Course Road, along with construction crews and sanitation staff. Their shifts had been planned the way they're planned every other day.
The weather app showed a hot day, not a lethal one. That's part of the problem: air temperature alone hides the danger. The risk comes from heat plus humidity plus direct sun, and only WBGT captures all three.
We spent the WeMakeDevs × AWS Environmental Hacks building HeatShift, a tool that gives a fleet manager a heat-safe plan for tomorrow every evening. This post covers what we built and how AWS made it possible. It also covers the one decision we're proudest of: a feature we refused to build.
Dashboard on the 28 May replay with the red EXTREME banner, the WBGT heatmap and the KPI tiles




The obvious fix, and why it backfires

Ask most people how to "solve" heat for gig workers and they'll suggest heat surge pay: pay more per delivery when it's dangerously hot. It sounds generous, and platforms already use the same mechanism for rain and late-night demand.
We think it's the wrong lever, for three reasons:
  1. It pays people for taking the risk. A worker who can afford to skip the 1 pm slot will skip it. The bonus mostly pulls in workers who can't afford to say no, so the most financially desperate people end up doing the most dangerous hours.
  1. It hits new workers hardest. Heat tolerance comes from acclimatisation, which takes days to build. US OSHA notes that most heat-related deaths at work happen in a worker's first few days on the job. New joiners are often the ones who need the money most, and a surge bonus targets exactly them.
  1. It turns safety into an individual choice. Once it's "your call whether to work at 1 pm for ₹40 extra," any harm becomes the worker's fault.
So we set one rule for HeatShift:
Move the work, not the risk.
Dangerous hours are taken off the schedule entirely, for everyone in the zone. Nobody can opt in. Lost income is covered by guaranteed pay, not paid out as a bonus for taking the risk.



Making the rule impossible to switch on

A policy that lives only in a slide deck lasts until the first busy week. So we built the rule into the API.
The fleet policy endpoint ( PUT /fleets/{id}/policy ) lets a manager tune most settings: the operating window, the hourly rate, the new-joiner threshold. One key is merged in last, after whatever the caller sends:
Python
LOCKED_POLICY = {"heat_surge_pay": False} # never configurable, by design

def update_policy(fid, body, **_):
meta = store.get(fid, "META")
allowed = {k: body[k] for k in DEFAULT_POLICY if k in body}
meta["policy"] = {**DEFAULT_POLICY, **meta.get("policy", {}), **allowed, **LOCKED_POLICY}
...
You can send  "heat_surge_pay": true  and it will save as  false  every time. It's a small piece of code, but it means the feature can't be quietly switched on later.
The rest of the system is built around the same rule:
  • Stop windows apply to the whole zone. On 28 May, HeatShift set "Cyber City: no outdoor work 11:00–15:00 (EXTREME, WBGT 33.2 °C)" and restructured all 20 shifts in that zone into cooler hours.
  • Pay is protected. Rest breaks are paid, and planned hours are guaranteed even when the plan shortens a shift.
  • Dispatch is blocked, not just discouraged. Messages can go unread, so HeatShift exposes a dispatch check that any routing system can call before assigning a job (example below). A manager can override it, but only with a written reason, and every override goes to an audit log.
  • New joiners get stricter limits. Anyone with fewer than 14 days on the job has every threshold applied 2.5 °C lower.
  • Reporting "unwell" is safe. Every worker gets an "I feel unwell" button. Tapping it alerts the manager instantly and never affects pay or rating. A safety button only works if workers aren't afraid to press it.
  • Customers are told, not dropped. Orders that fall inside a stop window are moved to cooler hours, with a ready-made message for the customer.
Here's the dispatch check refusing a 13:30 job in Cyber City on 28 May:
Plain text
GET /fleets/demo-gurugram/dispatch-check?zone=Z1&at=2026-05-28T13:30

{"allowed": false, "band": "EXTREME", "wbgt": 33.2,
"reason": "EXTREME heat stress (WBGT 33.2°C): no outdoor work in this window"}



What the manager and the worker see

The fleet manager gets the plan at 18:00 the evening before: a zone × hour WBGT heatmap, a map that follows the selected hour, stop windows, restructured shifts, the paid break schedule, how much water and ORS to stock per zone, and the cost of protection. Zones without a cool rest point within 2 km are flagged with a suggested partner location. One click, Approve & notify workers, releases the plan.
The worker gets a message in Hindi or English and a simple mobile card: today's heat-safe shift, break times, the nearest cool rest point with directions, a water target, the guaranteed pay for the day, and two buttons: I'm OK and I feel unwell.
The Worker messages tab on the dashboard

Worker card on a phone, W009, 28 May




Why we built it this way on AWS

We had a few days, so every service had to save us work rather than create it. Here's the architecture and the reason behind each choice.

AWS Step Functions: a plan that never silently fails to arrive

A heat plan that doesn't arrive is worse than having none, because the manager assumes tomorrow is fine. So the daily run is a Step Functions Standard workflow:
Fetch forecast → Compute heat risk (WBGT per zone-hour) → Plan shifts → Notify manager
Each step is a small, single-purpose Lambda function. The forecast fetch calls an external weather API, so it retries 3 times with exponential backoff. Every step has a  Catch  that routes to a Plan failed branch, which emails the manager that there's no plan instead of saying nothing. A CloudWatch alarm on  ExecutionsFailed  does the same thing as a second layer.
Step Functions also gave us a feature we hadn't planned. The execution history already records the state of every step, so the API reads it with  GetExecutionHistory , and the dashboard shows a live progress bar when a manager presses Re-plan. We didn't build any separate progress tracking. The workflow engine already had the data.

Amazon EventBridge Scheduler: two runs a day, in the right time zone

Heat plans are tied to local clock times, so the schedule had to be in IST, not UTC. EventBridge Scheduler supports a schedule time zone directly:
  • 18:00 IST: plan tomorrow, so the manager has the evening to approve it.
  • 05:30 IST: re-plan today with the freshest forecast. The manager is alerted again only if the heat got worse, so routine refreshes don't create alert fatigue.
Both schedules are about ten lines of YAML in the SAM template.

AWS Lambda on Graviton (arm64): small and cheap

All the logic, including the WBGT engine, the planner and the API, is Python 3.12 on arm64 Lambda, using only the standard library and boto3. The workload is spiky: a few seconds of computation twice a day, plus API calls when someone opens the dashboard. A server would sit idle almost all the time, while Lambda costs nothing between runs, and Graviton is cheaper per millisecond than x86.

Amazon DynamoDB: one table for the whole fleet

Everything a fleet owns sits under one partition key,  FLEET#<id> , with the sort key telling the item types apart:
Plain text
META fleet + policy
ZONE#<id> / WORKER#<id> / REST#<id>
RISK#<date>#<zone> hourly WBGT and risk band
PLAN#<date> KPIs, actions, worker cards
OUTBOX#<date> per-worker messages, built on approval
OVERRIDE#<date>#<ts> the override audit log
CHECKIN#<date>#<ts>#<worker> I'm OK / I feel unwell
Each dashboard view is a single  Query  with a  begins_with  prefix. For example, "all overrides on 28 May" is just a query on  OVERRIDE#2026-05-28 . With on-demand billing, a fleet that only plans twice a day pays almost nothing, and adding a fleet means adding a partition, not new infrastructure.

Amazon Location Service: real cool rest points, not pins we made up

Telling a worker to "take a break somewhere cool" doesn't help. They need a specific place nearby. We used the Amazon Location Service Places API to find real air-conditioned places, such as malls and fuel stations, near each zone. In Cyber City, the nearest one is 10 metres from the hub.
The same data drives a coverage-gap check: any zone without a rest point within 2 km is flagged, so the manager knows where to set up a partnership. The manager marks which suggested places they have an agreement with, and only those are sent to workers.

Amazon SNS: alerts when they matter

SNS sends four kinds of message: the evening brief, an alert when the morning refresh finds the heat has worsened, a workflow-failure alert, and the most important one, an instant alert when a worker taps "I feel unwell." That alert names the worker and the zone, tells the manager to call them now, and reminds them that the report never affects pay or rating.

Amazon S3: the forecast as it looked at the time

Every raw forecast snapshot is saved under  raw/fleet=<id>/date=<date>/ , a layout Athena can query directly. A lifecycle rule moves snapshots to Infrequent Access after 30 days. This keeps an audit trail of what the system knew when it made each decision, and it gives us the base for end-of-season reports such as exposure hours avoided per zone.

API Gateway (HTTP API), Amplify Hosting and AWS SAM

The React dashboard and worker card run on Amplify Hosting and call an HTTP API that routes to a single Lambda. The whole backend is one AWS SAM  template.yaml , deployed with  sam deploy . That includes 6 functions, the state machine, both schedules, the table, the bucket, the SNS topic, the alarm and least-privilege IAM policies for each function. Anyone can deploy their own copy with one command.
Project Architecture on AWS




The heat science, briefly (and its limits)

For each zone and hour, HeatShift pulls the forecast from Open-Meteo: temperature, humidity, wet-bulb temperature, solar radiation and wind. It then estimates outdoor WBGT, where Tw is wet-bulb temperature, Tg is globe temperature and Ta is air temperature:
Plain text
WBGT = 0.7 × Tw + 0.2 × Tg + 0.1 × Ta
Wet-bulb temperature (Tw) comes from the forecast, with the Stull (2011) formula as a fallback. Globe temperature (Tg) is estimated from solar radiation using the Hunter & Minyard formula. Each hour is then placed in a risk band based on ACGIH work/rest guidance for moderate work:
Level
WBGT
Rest per hour
Safe
< 28 °C
normal work
Caution
28 °C
15 min
Danger
29 °C
30 min
Severe
30 °C
45 min
Extreme
≥ 31.5 °C
no outdoor work
We're clear about the limits. This is a screening estimate from forecast data, not a measurement and not medical advice. It assumes moderate work in direct sun. A real deployment should calibrate against on-site WBGT meters.
We also made sure the demo isn't flattering itself. The 28 May replay uses Open-Meteo's archived forecasts, not the weather that actually happened. The plan you see is the one HeatShift would really have produced the evening before.



What fought back

A CORS preflight that blocked every write. The dashboard could read everything but couldn't approve, re-plan, override or check in. Every POST failed in the browser. The cause: our HTTP API used a catch-all  ANY /{proxy+}  route. Because that route also matched  OPTIONS , the browser's CORS preflight went to our Lambda function instead of being answered by the gateway's CORS configuration. Our router had no  OPTIONS  handler, so it returned 404, and the browser blocked every request after that. The fix was a few lines in the Lambda function to answer preflight requests. The lesson: if a proxy route matches  OPTIONS , your function has to handle preflight itself.
CloudFront wasn't available on a new account. Our plan was S3 + CloudFront for the frontend, but the account wasn't verified for CloudFront yet. We switched to Amplify Hosting, and it turned out to be a better fit: builds, HTTPS and a stable URL with no extra setup.
"Send it on WhatsApp" isn't a quick task in India. We designed the full outbox: on approval, HeatShift builds one message per worker in their language, on their channel. That means WhatsApp with I'm OK / I feel unwell quick replies, SMS, a Polly voice call in Hindi for workers on feature phones, or a webhook into a fleet's own app. But WhatsApp templates need Meta business verification, and commercial SMS needs DLT registration. So the demo builds and shows the outbox without sending it. That's also why the dispatch check is the real safety layer: messages explain the change, and the dispatch check enforces it whether or not anyone reads the message.



Results and potential impact

On the 28 May 2026 replay, for a fleet of 120 workers across 6 Gurugram zones:

Usual shifts
HeatShift plan
Worker-hours in Danger or worse
732
9
Shifts restructured
0
120
Capacity change
0%
−3.5%
Cost of protection (paid breaks + guaranteed hours)
₹0
₹4,050
That's a 98.8% reduction in dangerous exposure for about ₹34 per worker for the day, and the fleet keeps 96.5% of its capacity. Most of the work isn't cancelled, just moved to hours when it's safe.
The cost matters as much as the reduction. A safety measure that costs a fleet a third of its day won't last a summer. One that costs about the price of a cup of chai per worker, and shows that cost to the manager up front, has a chance.
The potential reach is large because nothing in HeatShift is specific to Gurugram or to delivery:
  • Any outdoor workforce with a roster can use it, including construction sites, sanitation departments, logistics yards and street vendors' cooperatives. Onboarding is a zones list and a roster CSV, about 15 minutes.
  • Any city works, because the forecast and Places data cover the whole country.
  • Adding a fleet adds data, not infrastructure. Each fleet is a new DynamoDB partition and a new schedule input, and the serverless stack costs nothing while idle.
  • The dispatch check is one HTTP call, so platforms can adopt the hard stop without replacing their routing systems.
Heatwaves in India are getting longer and more humid. Cities have heat action plans. HeatShift is our attempt to bring the same thinking down to the level of individual shifts, decided the evening before.



What's next

  • Amazon Cognito sign-in for managers, and signed links for worker cards
  • AWS End User Messaging for live WhatsApp and SMS delivery once registration is done
  • AWS IoT Core to bring in on-site WBGT sensor data and calibrate the forecast estimate
  • Athena + Glue season reports over the S3 forecast data: exposure hours avoided, overrides, unwell reports per zone
  • IMD heatwave warnings combined with the risk model
  • Staffing-aware planning that balances restructured shifts against demand peaks



Try it

Built for the WeMakeDevs × AWS Environmental Hacks (Heat & Water track). We used Claude Code as an AI coding assistant during the build. The 120-person roster is synthetic demo data; the weather is real.
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