Nirmitee.io
TelehealthFHIR

Remote Patient Monitoring Data Pipeline: From Wearable Device to FHIR Observation to Clinical Alert

April 23, 202615 min readUpdated Oct 19, 2026
Written by
Dinesh Thorat
Dinesh Thorat

Digital Growth Lead

Writes about healthcare technology, interoperability, and AI-driven transformation across modern care systems.

Remote Patient Monitoring Data Pipeline: From Wearable Device to FHIR Observation to Clinical Alert

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.

For hands-on help with this, see our remote patient monitoring software development.

Related case study: we ran this pipeline pattern at scale in a diabetes care platform with CGM integration and AI insulin.

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. Our FHIR Observation resource guide covers the vital-signs LOINC codes, UCUM units and category values these normalized readings should carry.

LOINC Code Mapping

Vital SignLOINC CodeLOINC DisplayUnit
Blood Pressure (panel)85354-9Blood pressure panelmmHg
Systolic BP8480-6Systolic blood pressuremmHg
Diastolic BP8462-4Diastolic blood pressuremmHg
Blood Glucose2339-0Glucose [Mass/volume] in Bloodmg/dL
SpO22708-6Oxygen saturation in Arterial blood%
Body Weight29463-7Body weightkg or lbs
Body Temperature8310-5Body temperatureCel or [degF]
Heart Rate8867-4Heart 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 SignCritical (Page Immediately)Urgent (1-Hour Notification)Routine (Daily Review)
Systolic BPOver 180 or under 80Over 160 or under 90Over 140
Diastolic BPOver 120 or under 50Over 100 or under 60Over 90
Blood GlucoseOver 500 or under 54 mg/dLOver 300 or under 70 mg/dLOver 200 mg/dL
SpO2Under 88%Under 92%Under 95%
Weight Change (CHF)Over 5 lbs/48 hrsOver 3 lbs/dayOver 2 lbs/day
Heart RateOver 150 or under 40 bpmOver 120 or under 50 bpmOver 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:

  1. 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
  2. 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
  3. 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.

See how this plays out in a real project: RPM Heart Failure Case Study: Reduce Readmissions.

Pipeline Technology Stack Recommendations

Based on production RPM deployments, the following technology stack provides the best balance of reliability, scalability, and healthcare compliance:

Pipeline StageRecommended TechnologyWhy
Message QueueApache Kafka or AWS SQSKafka for high-volume continuous monitoring; SQS for simpler deployments
Stream ProcessingApache Flink or AWS KinesisReal-time rule evaluation with exactly-once processing guarantees
FHIR ServerHAPI FHIR or custom (Go/Java)HAPI for rapid deployment; custom for performance-critical paths
Time-Series StorageTimescaleDB or InfluxDBOptimized for device telemetry queries and downsampling
Alert RoutingCustom service + Twilio/SNSSMS/push delivery with acknowledgment tracking
EHR IntegrationMirth Connect or custom FHIR clientMirth 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 time zones?

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.

Related Reading

For more insights, explore our guides on Medical Device Integration FHIR IoMT Ieee 11073 Devicealert and FHIR in Modern Healthcare.

You may also find value in EHR Software Development Guide and Building a Healthtech App.

Ready to scale?

Talk to our healthcare engineering team about building, integrating, and shipping faster.

Frequently Asked Questions

What is a remote patient monitoring (RPM) data pipeline?

An RPM data pipeline is the engineering layer that transforms raw readings from devices like blood pressure cuffs, glucose meters, and weight scales into clinically actionable alerts delivered to the right clinician at the right time. It spans device data ingestion over BLE or cellular, normalization of vendor formats into FHIR Observations with LOINC coding, a clinical rules engine for thresholds, alert routing, and EHR write-back.

How much data does a remote patient monitoring program generate?

A single RPM patient with a blood pressure cuff, glucose meter, and weight scale generates 10-15 readings per day, so a practice monitoring 500 patients processes 5,000-7,500 data points daily. Without a well-architected pipeline, that volume overwhelms clinical staff and creates the alert fatigue that undermines RPM programs. The market context matches: RPM is projected to grow from $27.7 billion in 2024 to $57 billion by 2030.

How do you convert wearable device data into FHIR Observations?

A normalization layer transforms each vendor's proprietary payload into standardized FHIR Observation resources with LOINC coding, since every vendor formats data differently; Omron sends blood pressure as simple systolic/diastolic keys while Withings uses typed measure arrays. Standard mappings include LOINC 85354-9 for the blood pressure panel, 2339-0 for blood glucose, 2708-6 for SpO2, and 29463-7 for body weight, with UCUM units in each valueQuantity.

What vital sign thresholds should trigger RPM clinical alerts?

A clinical rules engine grades each reading into severity tiers: critical alerts that page a clinician immediately (systolic BP over 180 or under 80, glucose over 500 or under 54 mg/dL, SpO2 under 88%), urgent alerts notified within an hour (SpO2 under 92%, glucose over 300 mg/dL), and routine readings batched for daily review. Static thresholds are the starting point; personalized patient baselines are the goal.

What is the difference between BLE and cellular RPM devices?

BLE devices transmit readings to a mobile phone or hub that acts as a gateway forwarding data to a cloud API, using standard profiles like the Blood Pressure Profile and Glucose Profile. Cellular-connected devices, such as those from Tenovi, BodyTrace, and Biobeat, include built-in modems and send data directly to the vendor cloud, removing patient friction like app installation and Bluetooth pairing but adding vendor API dependencies to your pipeline.

How does Nirmitee.io approach healthcare software development?

Nirmitee.io follows a healthcare-first engineering approach — every solution is built with HIPAA compliance, HL7/FHIR interoperability standards, and clinical workflow requirements at the core. We combine domain expertise with modern engineering practices to deliver production-ready systems.
Share