The RPM Data Challenge: From Raw Device Readings to Clinical Intelligence
The Remote Patient Monitoring market is projected to grow from $27.7 billion in 2024 to $57 billion by 2030. But the technology challenge is not collecting data — modern BLE-enabled medical devices can transmit readings automatically every few minutes. The real engineering challenge is building a data pipeline that transforms hundreds of daily device readings per patient into clinically actionable insights that arrive at the right clinician at the right time.
A single RPM patient with a blood pressure cuff, glucose meter, and weight scale generates 10-15 readings per day. A practice monitoring 500 patients processes 5,000-7,500 data points daily. Without a well-architected pipeline, this volume overwhelms clinical staff and creates the alert fatigue problem that undermines RPM programs. This guide covers every stage of the pipeline, from BLE data ingestion through FHIR normalization to clinical alert routing and EHR write-back.
Stage 1: Device Data Ingestion
RPM devices communicate through several transport protocols. Understanding each is critical for building a reliable ingestion layer.
BLE (Bluetooth Low Energy) Devices
Most consumer and clinical-grade RPM devices use BLE to communicate with a mobile phone or hub device. The mobile app acts as a gateway, receiving BLE data and forwarding it to the cloud API. Common BLE health profiles include:
- Blood Pressure Profile (BLP): Systolic, diastolic, mean arterial pressure, pulse rate, timestamp
- Glucose Profile (GLP): Glucose concentration, type (capillary/venous), meal context, timestamp
- Weight Scale Profile (WSP): Weight, BMI (if height configured), timestamp
- Pulse Oximeter Profile: SpO2 percentage, pulse rate, perfusion index
Cellular-Connected Devices
Some RPM devices (particularly those from Tenovi, BodyTrace, and Biobeat) include built-in cellular modems. These devices transmit data directly to the vendor cloud without requiring a mobile phone gateway. This eliminates patient friction (no app installation, no Bluetooth pairing) but adds vendor API dependencies to your pipeline.
# Python: Unified device data ingestion handler
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Optional
class VitalType(Enum):
BLOOD_PRESSURE = "blood_pressure"
GLUCOSE = "glucose"
WEIGHT = "weight"
SPO2 = "spo2"
TEMPERATURE = "temperature"
@dataclass
class RawDeviceReading:
patient_id: str
device_id: str
device_vendor: str
vital_type: VitalType
values: dict # vendor-specific format
timestamp: datetime
transmission_method: str # "ble_gateway", "cellular", "wifi"
class DeviceIngestionService:
def __init__(self, message_queue):
self.queue = message_queue
self.vendor_parsers = {
"omron": OmronParser(),
"withings": WithingsParser(),
"tenovi": TenoviParser(),
"ihealth": IHealthParser(),
"bodyTrace": BodyTraceParser(),
}
async def ingest(self, vendor: str, raw_payload: dict) -> RawDeviceReading:
parser = self.vendor_parsers.get(vendor)
if not parser:
raise ValueError(f"Unsupported vendor: {vendor}")
reading = parser.parse(raw_payload)
# Validate device is registered and linked to a patient
patient = await self.lookup_patient(reading.device_id)
reading.patient_id = patient.id
# Publish to normalization queue
await self.queue.publish("rpm.raw_readings", reading)
return reading Stage 2: Data Normalization (Vendor Formats to FHIR Observations)
Every device vendor uses a different data format. Omron sends blood pressure as {"systolic": 128, "diastolic": 82, "pulse": 72}. Withings sends it as {"measures": [{"value": 128, "type": 10}, {"value": 82, "type": 9}]}. The normalization layer transforms all vendor formats into standardized FHIR Observation resources with proper LOINC coding.
LOINC Code Mapping
| Vital Sign | LOINC Code | LOINC Display | Unit |
|---|---|---|---|
| Blood Pressure (panel) | 85354-9 | Blood pressure panel | mmHg |
| Systolic BP | 8480-6 | Systolic blood pressure | mmHg |
| Diastolic BP | 8462-4 | Diastolic blood pressure | mmHg |
| Blood Glucose | 2339-0 | Glucose [Mass/volume] in Blood | mg/dL |
| SpO2 | 2708-6 | Oxygen saturation in Arterial blood | % |
| Body Weight | 29463-7 | Body weight | kg or lbs |
| Body Temperature | 8310-5 | Body temperature | Cel or [degF] |
| Heart Rate | 8867-4 | Heart rate | /min |
FHIR Observation: Blood Pressure Example
// FHIR Observation for blood pressure reading from RPM device
{
"resourceType": "Observation",
"status": "final",
"category": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs",
"display": "Vital Signs"
}]
}],
"code": {
"coding": [{
"system": "http://loinc.org",
"code": "85354-9",
"display": "Blood pressure panel"
}]
},
"subject": { "reference": "Patient/patient-12345" },
"device": { "reference": "Device/omron-vs-88421" },
"effectiveDateTime": "2026-03-16T08:30:00-05:00",
"component": [
{
"code": {
"coding": [{ "system": "http://loinc.org", "code": "8480-6", "display": "Systolic BP" }]
},
"valueQuantity": { "value": 142, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" }
},
{
"code": {
"coding": [{ "system": "http://loinc.org", "code": "8462-4", "display": "Diastolic BP" }]
},
"valueQuantity": { "value": 91, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]" }
}
]
}
Stage 3: Clinical Rules Engine
The rules engine evaluates every normalized observation against clinical thresholds to determine whether an alert is warranted. Static thresholds are the starting point; personalized baselines are the goal.
Default Clinical Alert Rules
| Vital Sign | Critical (Page Immediately) | Urgent (1-Hour Notification) | Routine (Daily Review) |
|---|---|---|---|
| Systolic BP | Over 180 or under 80 | Over 160 or under 90 | Over 140 |
| Diastolic BP | Over 120 or under 50 | Over 100 or under 60 | Over 90 |
| Blood Glucose | Over 500 or under 54 mg/dL | Over 300 or under 70 mg/dL | Over 200 mg/dL |
| SpO2 | Under 88% | Under 92% | Under 95% |
| Weight Change (CHF) | Over 5 lbs/48 hrs | Over 3 lbs/day | Over 2 lbs/day |
| Heart Rate | Over 150 or under 40 bpm | Over 120 or under 50 bpm | Over 100 bpm |
# Python: Clinical rules engine for RPM alerts
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class AlertSeverity(Enum):
CRITICAL = "critical" # Page clinician immediately
URGENT = "urgent" # In-app notification within 1 hour
ROUTINE = "routine" # Batch for daily review
NORMAL = "normal" # No alert
@dataclass
class ClinicalAlert:
patient_id: str
severity: AlertSeverity
vital_type: str
value: float
threshold: float
message: str
observation_id: str
class BloodPressureRules:
def evaluate(self, systolic: float, diastolic: float) -> AlertSeverity:
# Critical: hypertensive crisis or hypotension
if systolic > 180 or systolic < 80 or diastolic > 120 or diastolic < 50:
return AlertSeverity.CRITICAL
# Urgent: stage 2 hypertension
if systolic > 160 or diastolic > 100 or systolic < 90 or diastolic < 60:
return AlertSeverity.URGENT
# Routine: stage 1 hypertension
if systolic > 140 or diastolic > 90:
return AlertSeverity.ROUTINE
return AlertSeverity.NORMAL
class GlucoseRules:
def evaluate(self, glucose_mg_dl: float) -> AlertSeverity:
if glucose_mg_dl > 500 or glucose_mg_dl < 54:
return AlertSeverity.CRITICAL
if glucose_mg_dl > 300 or glucose_mg_dl < 70:
return AlertSeverity.URGENT
if glucose_mg_dl > 200:
return AlertSeverity.ROUTINE
return AlertSeverity.NORMAL
class WeightChangeRules:
def evaluate(self, weight_change_lbs: float, hours: int) -> AlertSeverity:
daily_rate = weight_change_lbs / (hours / 24)
if weight_change_lbs > 5 and hours <= 48:
return AlertSeverity.CRITICAL
if daily_rate > 3:
return AlertSeverity.URGENT
if daily_rate > 2:
return AlertSeverity.ROUTINE
return AlertSeverity.NORMAL Stage 4: Alert Routing and Escalation
Different alert severities require different delivery channels and response time expectations. The alert routing system must match urgency to communication method and include escalation paths for unacknowledged alerts.
Routing Rules
- Critical alerts: SMS + push notification to the on-call clinician. If not acknowledged within 5 minutes, escalate to the supervising physician. If still unacknowledged after 10 minutes, escalate to the clinical director. Log all escalation timestamps for compliance
- Urgent alerts: In-app notification to the assigned care coordinator. Appears on the team dashboard with a 1-hour response timer. If unacknowledged after 1 hour, upgrade to SMS delivery
- Routine alerts: Batched into a daily digest email sent to the care team at 7 AM. Also visible on the daily review dashboard sorted by patient risk score. No individual notification for each routine alert
# Python: Alert routing service
import asyncio
from datetime import datetime, timedelta
class AlertRouter:
def __init__(self, sms_service, push_service, email_service, dashboard):
self.sms = sms_service
self.push = push_service
self.email = email_service
self.dashboard = dashboard
async def route_alert(self, alert: ClinicalAlert):
# Always log to dashboard
await self.dashboard.add_alert(alert)
if alert.severity == AlertSeverity.CRITICAL:
on_call = await self.get_on_call_clinician(alert.patient_id)
await self.sms.send(on_call.phone, alert.message)
await self.push.send(on_call.device_token, alert.message)
# Schedule escalation
asyncio.create_task(self.escalation_chain(alert, on_call))
elif alert.severity == AlertSeverity.URGENT:
coordinator = await self.get_care_coordinator(alert.patient_id)
await self.push.send(coordinator.device_token, alert.message)
asyncio.create_task(self.escalate_if_unacked(
alert, coordinator, timeout=timedelta(hours=1)
))
elif alert.severity == AlertSeverity.ROUTINE:
await self.add_to_daily_digest(alert) Stage 5: EHR Write-Back
The final pipeline stage writes normalized observations back to the patient's electronic health record. This is essential for clinical continuity — physicians reviewing a patient's chart in Epic or Cerner must see RPM data alongside in-office vitals. For detailed integration patterns, see our guide on EHR integration with wearable devices.
Write-back requires careful handling of several integration challenges:
- Duplicate detection: If a patient takes two blood pressure readings 30 seconds apart, are they duplicates or distinct measurements? Implement a configurable deduplication window (typically 2-5 minutes) per vital type
- Timezone handling: Device timestamps may be in the patient's local timezone, but the EHR stores data in UTC or the facility timezone. Always normalize to UTC at ingestion and convert for display
- Data quality validation: Reject physiologically impossible readings (systolic BP of 500, weight of 20 lbs for an adult). Flag suspicious readings for manual review rather than auto-filing to the chart
- Source attribution: Every RPM observation written to the EHR must include a Provenance resource identifying the device, the RPM platform, and the data pathway. This distinguishes RPM data from in-office measurements
Edge Processing: Reducing Data Volume by 83%
Continuous monitoring devices like glucose monitors and pulse oximeters can generate a reading every 5 seconds. For a practice with 500 patients, that is over 8.6 million data points per day — far too much for cloud processing and clinical review.
Edge processing on the mobile gateway device or hub reduces this volume by applying three filters before cloud upload:
- Moving average smoothing: Replace individual readings with 5-minute averages. A blood pressure reading every 5 minutes instead of every 30 seconds reduces volume by 10x
- Noise filtering: Discard readings that fall within the sensor's measurement error range of the previous reading. If SpO2 fluctuates between 97% and 98%, only upload when it changes by more than 2 percentage points
- Anomaly-only upload: For stable patients, only upload readings that deviate from the established baseline by a clinically significant amount. Upload all readings when a threshold is breached
This edge processing approach achieves an 83% reduction in cloud data upload volume while preserving all clinically significant data points. The cost savings on cloud compute and storage are substantial: a 500-patient RPM program can reduce monthly cloud costs from approximately $2,400 to $400 through edge filtering.
Pipeline Technology Stack Recommendations
Based on production RPM deployments, the following technology stack provides the best balance of reliability, scalability, and healthcare compliance:
| Pipeline Stage | Recommended Technology | Why |
|---|---|---|
| Message Queue | Apache Kafka or AWS SQS | Kafka for high-volume continuous monitoring; SQS for simpler deployments |
| Stream Processing | Apache Flink or AWS Kinesis | Real-time rule evaluation with exactly-once processing guarantees |
| FHIR Server | HAPI FHIR or custom (Go/Java) | HAPI for rapid deployment; custom for performance-critical paths |
| Time-Series Storage | TimescaleDB or InfluxDB | Optimized for device telemetry queries and downsampling |
| Alert Routing | Custom service + Twilio/SNS | SMS/push delivery with acknowledgment tracking |
| EHR Integration | Mirth Connect or custom FHIR client | Mirth for HL7v2 legacy EHRs; FHIR client for modern systems |
For organizations evaluating integration platforms, our guide on choosing the right healthcare integration platform covers the decision framework for selecting between Mirth Connect, Rhapsody, and custom solutions.
Frequently Asked Questions
What LOINC code should I use for blood pressure from an RPM device?
Use LOINC 85354-9 (Blood pressure panel) as the parent observation code, with components 8480-6 (Systolic) and 8462-4 (Diastolic). This matches the US Core vital signs profile and ensures interoperability with all major EHR systems.
How do I handle continuous glucose monitor (CGM) data volume?
CGMs like Dexcom G7 generate a reading every 5 minutes (288 readings/day). Apply edge processing to upload only hourly averages and any readings outside the target range (typically 70-180 mg/dL). For RPM billing purposes under CPT 99454, you need at least one transmission per day for 16+ days.
Should RPM data be stored in the EHR or in a separate system?
Both. Store the complete time-series dataset in a purpose-built time-series database (TimescaleDB, InfluxDB) for analytics, trending, and clinical dashboards. Write clinically significant observations to the EHR as FHIR Observations for integration into the longitudinal patient record. The EHR is the system of record for clinical documentation; the time-series database is the analytical engine.
How do I handle RPM data from patients who travel across timezones?
Always store timestamps in UTC at the pipeline level. The device or gateway app should capture the local timezone at the time of measurement and include it as metadata. When displaying data to clinicians, convert to the clinician's timezone. When displaying to patients, convert to their current timezone. This prevents confusion when a patient in EST takes a reading while traveling in PST.
What happens when a device loses connectivity?
Design the mobile gateway app to store readings locally when offline and batch-upload when connectivity returns. Include a "data received timestamp" alongside the "measurement timestamp" so the pipeline can distinguish between real-time and delayed data. Delayed data should still trigger alert evaluation but with an adjusted urgency — a critical BP reading from 4 hours ago may no longer warrant an immediate page.
How do I validate that device data is accurate?
Implement physiological plausibility checks: systolic BP between 60-280 mmHg, diastolic between 30-180 mmHg, glucose between 20-600 mg/dL, SpO2 between 50-100%, weight within 50% of the patient's baseline. Readings outside these ranges are likely measurement errors (wrong cuff position, sensor malfunction) and should be flagged for review rather than auto-filed.
Sources: IEEE 11073 Personal Health Device Communication Standards, HL7 FHIR R4 Observation Resource Specification, LOINC Code Database, CMS Remote Patient Monitoring Guidelines 2026, Dexcom G7 Technical Specifications.




