ESP32 Modules Research

ESP32 Modules Research for Rust Development

Project Goal

Build an ESP32-based sensor system that uploads data directly to S3/Object Storage as Parquet files, programmed in Rust.


Executive Summary

BREAKTHROUGH: Native Parquet on ESP32-S3 is PROVEN FEASIBLE!

Proven Architecture

Loading diagram...

POC Results (December 2025) - VERIFIED ON ESP32-S3!

Configuration Binary Size Partition Used Status
Uncompressed 989 KB 24.52% ✅ Works
Snappy (pure Rust) 997 KB 24.73% Recommended
ZSTD (C library) N/A N/A ❌ Cross-compile fails
  • parquet-rs v57.1.0 compiles and works on ESP32-S3
  • Snappy compression (pure Rust) - recommended, +8 KB binary overhead
  • rusty-s3: Sans-IO S3 client for presigned URLs
  • Binary size: 997 KB (only 24.73% of partition!)
  • Memory: ~110 KB peak (fits in 8MB PSRAM easily!)

⚠️ Important: ZSTD does NOT work for ESP32 cross-compilation due to C library endianness issues. Use Snappy instead!

Rank Module Best For
1 ESP32-S3 Native Parquet + Snappy + 8MB PSRAM
2 ESP32-C6 WiFi 6 + standard RISC-V toolchain
3 ESP32-C61 Future-proof (WiFi 6 + PSRAM) - limited availability
4 ESP32-P4 Maximum power - requires external WiFi chip

Detailed Module Comparison

ESP32-S3 Specifications

Loading diagram...
Specification Value
Architecture Dual-core Xtensa LX7 @ 240 MHz
On-chip SRAM 512 KB
External PSRAM Up to 16MB (Octal SPI)
Flash Options 8MB / 16MB / 32MB
WiFi 802.11 b/g/n (2.4 GHz)
Bluetooth BLE 5.0
GPIO 45 programmable pins
USB USB 2.0 OTG full-speed
Performance 1329 CoreMark

Key Features:

  • AI acceleration with vector instructions
  • Camera (MIPI-CSI) and LCD support
  • Largest PSRAM capacity among WiFi-enabled variants

Rust Support:

  • Fully supported by esp-hal 1.0 (stable)
  • Requires Xtensa fork of Rust compiler
  • Both no_std and std (ESP-IDF) approaches available
  • WiFi via esp-radio crate

Recommended Development Boards:

Board Flash PSRAM Price Range
Waveshare ESP32-S3-DEV-KIT-N16R8 16MB 8MB ~$15
Adafruit Metro ESP32-S3 16MB 8MB ~$25
LILYGO T7-S3 16MB 8MB ~$12
Unexpected Maker TinyS3 8MB 8MB ~$22

ESP32-C6 (Best Standard Toolchain)

ESP32-C6 Specifications

Loading diagram...
Specification Value
Architecture Single-core RISC-V @ 160 MHz
Low-Power Core RISC-V @ 20 MHz
On-chip SRAM 512 KB
External PSRAM Not supported
Flash 8MB (typical)
WiFi 802.11ax (WiFi 6) - 2.4 GHz only
Bluetooth BLE 5.3
802.15.4 Thread / Zigbee / Matter
GPIO 22-30 programmable pins
Performance 496 CoreMark

Key Features:

  • First ESP32 with WiFi 6
  • Thread and Zigbee support (Matter-ready)
  • 2x TWAI (CAN) controllers
  • USB Serial/JTAG controller

Rust Support:

  • Fully supported by esp-hal 1.0 (stable)
  • Standard RISC-V toolchain (no fork needed!)
  • Full std library support via ESP-IDF
  • Async/await with Embassy executor

Critical Limitation:

No PSRAM support means only 512KB SRAM available. This significantly limits data buffering capacity for your S3 upload use case.

Recommended Development Boards:

Board Flash Notes
ESP32-C6-DevKitC-1 8MB Official Espressif board
ESP32-C6 Super Mini 4MB Compact form factor
FireBeetle 2 ESP32-C6 8MB DFRobot ecosystem

ESP32-C61 (Future Option)

ESP32-C61 Specifications

Loading diagram...
Specification Value
Architecture Single-core RISC-V @ 160 MHz
On-chip SRAM 320 KB (less than C6)
External PSRAM Supported (Quad SPI @ 120 MHz)
WiFi 802.11ax (WiFi 6) - optimized for 20 MHz
Bluetooth BLE 5.0 with long-range
Security TEE, Secure boot, Flash/PSRAM encryption

Release Status:

  • Announced: January 2024
  • Status: Development boards becoming available in 2025
  • Mass production timeline unclear

Why Wait for C61?

  • Combines WiFi 6 (from C6) + PSRAM support (from S3)
  • Standard RISC-V toolchain
  • Cost-optimized

ESP32-P4 (Maximum Power)

ESP32-P4 Specifications

Loading diagram...
Specification Value
Architecture Dual-core RISC-V @ 400 MHz
Low-Power Core RISC-V @ 40 MHz
On-chip SRAM 768 KB
External PSRAM Up to 32MB
WiFi/Bluetooth None - requires companion chip
Video H.264 encoding 1080p@30fps
Display MIPI-DSI (up to 1080p)
Camera MIPI-CSI
USB USB 2.0 High-Speed OTG
GPIO 55 programmable pins

Use Case: HMI, video doorbells, edge AI - overkill for sensor data logging


Specification Value
Architecture Single-core RISC-V @ 96 MHz
SRAM 320 KB
WiFi None
Bluetooth BLE 5.0
802.15.4 Thread / Zigbee

Not suitable: No WiFi capability - cannot upload to S3 directly.


Complete Comparison Table

Processing Power Comparison

Loading diagram...
Feature ESP32-S3 ESP32-C6 ESP32-C61 ESP32-P4 ESP32-H2
CPU 2x Xtensa 240MHz 1x RISC-V 160MHz 1x RISC-V 160MHz 2x RISC-V 400MHz 1x RISC-V 96MHz
SRAM 512 KB 512 KB 320 KB 768 KB 320 KB
PSRAM 16MB None Yes 32MB None
WiFi WiFi 4 WiFi 6 WiFi 6 None None
Bluetooth BLE 5.0 BLE 5.3 BLE 5.0 None BLE 5.0
Thread/Zigbee No Yes No No Yes
Rust Toolchain Xtensa fork Standard Standard Standard Standard
esp-hal 1.0 Stable Stable Coming In dev Stable
WiFi in Rust esp-radio esp-radio Expected In progress N/A
Availability Wide Wide Limited Samples Wide
For This Project BEST Good Future Overkill No WiFi

Rust Ecosystem Overview

esp-hal 1.0.0 (October 2025)

ESP-RS Ecosystem

Loading diagram...

Two Development Approaches

Development Approaches

Loading diagram...
Aspect no_std (Bare Metal) std (ESP-IDF)
Binary Size Smaller Larger
Control Full hardware control Higher-level abstractions
Networking esp-radio (experimental) Mature ESP-IDF stack
Standard Library No Vec, String, etc. Full std library
HTTP Client Manual implementation esp-idf-svc::http
Recommended For Resource-constrained Your S3 project

Parquet/Arrow Deep Dive: Can ESP32 Create Parquet Files?

Real-World Reference: opensensor.space

Your actual Parquet files from the Raspberry Pi Zero W setup:

Metric Value
File Size ~11.6 KB
Rows 178 (15 minutes @ 5 sec intervals)
Columns 20 sensor readings (floats)
Compression Snappy
Encoding PLAIN + RLE_DICTIONARY
Row Groups 1

This is MUCH smaller than typical Parquet use cases!

Updated Analysis: Small Parquet Files MIGHT Be Feasible

Parquet Feasibility Analysis

Loading diagram...

Rust Parquet/Arrow Crates Analysis

parquet-rs (arrow-rs ecosystem)

Aspect Status Details
no_std support None Requires heap allocation, Vec, HashMap, String
Dependencies Heavy Thrift, compression libs (snappy, gzip, zstd)
Min row group ~5MB Practical minimum for compression efficiency
Memory for write ~2x row group Buffering required before flush

Verdict: Cannot run on ESP32

arrow-rs

Aspect Status Details
no_std support None Designed for in-memory columnar processing
Memory High Requires megabytes of RAM

Verdict: Cannot run on ESP32

Arrow IPC / Feather V2

Aspect Status Details
Complexity Lower No Thrift, simpler than Parquet
File size ~45% larger Less compression than Parquet
Memory ~1-2MB min Still requires batch buffering

Verdict: Still too heavy, but closest option


Parquet File Structure (Why It's Memory-Hungry)

Parquet File Structure

Loading diagram...

Minimum Memory Requirements:

  • Row group buffer: 1-5 MB (minimum practical size)
  • Thrift serialization: ~100-500 KB
  • Compression workspace: ~100 KB - 1 MB
  • Total: 2-7 MB minimum - exceeds ESP32 practical limits

Lightweight Alternatives for ESP32

Comparison Table

Format Memory no_std Compression ESP32 Suitable
Postcard ~1-5 KB Yes Good (binary) Best
CBOR ~1-5 KB Yes Good (binary) Excellent
MessagePack ~5-10 KB Partial Good (binary) Good
Gorilla TSC ~1 KB Yes 12x for time-series Best for sensors
Sprintz Less than 1 KB Yes Excellent Purpose-built for IoT
Protocol Buffers ~10-20 KB Partial 3x smaller than JSON Good
JSON ~10-20 KB Partial None Large output
Arrow IPC ~1-2 MB No Moderate Too heavy
Parquet ~5+ MB No Best Impossible

// Cargo.toml
[dependencies]
postcard = "1.0"
serde = { version = "1.0", default-features = false, features = ["derive"] }

// Example usage
use postcard::{to_vec, from_bytes};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct SensorReading {
    timestamp: u64,
    temperature: f32,
    humidity: f32,
    pressure: f32,
}

fn serialize_readings(readings: &[SensorReading]) -> Vec<u8> {
    postcard::to_allocvec(readings).unwrap()
}

Why Postcard:

  • Designed specifically for #![no_std] microcontrollers
  • Stable wire format (v1.0+)
  • Extremely compact binary output
  • Drop-in replacement for Serde

Gorilla Compression

Loading diagram...

Rust Crates:

  • tsz-rs - Gorilla implementation
  • gorilla-tsc - Alternative implementation
// Ideal for sensor data with regular intervals
// Timestamps: 10:00:00, 10:00:01, 10:00:02... -> nearly free
// Temperatures: 23.5, 23.5, 23.6, 23.5... -> highly compressible

// Cargo.toml
[dependencies]
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_cbor = { version = "0.11", default-features = false }

// Usage identical to JSON but binary output
let data = SensorReading { ... };
let bytes = serde_cbor::to_vec(&data)?;

Sprintz Algorithm (Purpose-Built for IoT)

Feature Value
Designed for IoT and resource-constrained devices
Memory required Less than 1 KB
Latency Virtually zero
Best for Predictable time series, monotonic values

Perfect for: Temperature, humidity, pressure sensors with regular intervals


PROVEN: Native Parquet on ESP32-S3 is FEASIBLE!

We built and tested a POC using parquet-rs v57.1.0 directly on ESP32-S3 hardware, and it works!

POC Repository: github.com/walkthru-earth/esp32s3-parquet-test

POC Test Results (December 2025) - VERIFIED ON ESP32-S3!

POC Results

Loading diagram...

Compression Comparison (ESP32-S3 Actual Build)

Compression Binary Size Partition Used Status
Uncompressed 989 KB 24.52% ✅ Works
Snappy (pure Rust) 997 KB 24.73% Recommended
ZSTD (C library) N/A N/A ❌ Cross-compile fails

Winner: Snappy - Pure Rust, cross-compiles perfectly, only +8 KB overhead.

Why ZSTD Fails on ESP32

Root cause: The zstd crate depends on zstd-sys which compiles the C zstd library using the host compiler instead of the ESP32 cross-compiler:

This results in:

  1. Wrong architecture (host vs Xtensa)
  2. Endianness mismatch (big-endian objects, little-endian target)
  3. Linker failure

Note: zstd-safe claims "no_std" support, but this only means the Rust wrapper doesn't use std. It still requires the C library via zstd-sys.

Memory Analysis - Confirmed!

Component Memory Needed
Raw sensor data (178 × 14 × 4 bytes) ~11 KB
Column buffers ~80 KB
Snappy workspace ~10 KB
Metadata ~10 KB
Total Peak Memory ~110 KB
ESP32-S3 PSRAM Available 8,000 KB

Conclusion: Memory is NOT a bottleneck - fits easily in PSRAM!


Working Cargo.toml for ESP32-S3 Parquet + S3

Compression Options for ESP32

Feature Binary Impact Pure Rust ESP32 Status Recommendation
snap +8 KB ✅ Yes ✅ Works Recommended
(none) baseline N/A ✅ Works Smallest binary
zstd N/A ❌ No (C lib) Fails Don't use
lz4 N/A ❌ No (C lib) ❌ Likely fails Don't use

Pure Rust Compression Crates (ESP32 Compatible)

Crate Version Pure Rust Parquet Integration Notes
snap 1.1.1 features = ["snap"] Best choice
lz4_flex 0.12.0 ❌ Not supported Parquet uses C lz4
ruzstd 0.8.2 ❌ Decoder only Cannot compress
zstd-safe 7.2.4 features = ["zstd"] Cross-compile fails

Working Parquet Writer Code (ESP32-S3)

use parquet::basic::{Compression, Encoding};
use parquet::data_type::{FloatType, Int64Type};
use parquet::file::properties::WriterProperties;
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::parser::parse_message_type;
use std::fs::File;
use std::sync::Arc;

fn write_parquet_file(readings: &[SensorReading], filename: &str) -> Result<()> {
    let message_type = "
        message sensor_data {
            required int64 timestamp (TIMESTAMP_MILLIS);
            required float temperature;
            required float humidity;
            required float pressure;
            // ... more columns
        }
    ";

    let schema = Arc::new(parse_message_type(message_type)?);

    // Snappy compression - pure Rust, works on ESP32!
    let props = WriterProperties::builder()
        .set_compression(Compression::SNAPPY)
        .set_encoding(Encoding::PLAIN)
        .set_statistics_enabled(parquet::file::properties::EnabledStatistics::None)
        .build();

    let file = File::create(filename)?;
    let mut writer = SerializedFileWriter::new(file, schema, Arc::new(props))?;
    let mut row_group_writer = writer.next_row_group()?;

    // Write each column using typed() API
    {
        let mut col_writer = row_group_writer.next_column()?.unwrap();
        col_writer
            .typed::<Int64Type>()
            .write_batch(&timestamps, None, None)?;
        col_writer.close()?;
    }

    // Float columns
    {
        let mut col_writer = row_group_writer.next_column()?.unwrap();
        col_writer
            .typed::<FloatType>()
            .write_batch(&temperatures, None, None)?;
        col_writer.close()?;
    }

    row_group_writer.close()?;
    writer.close()?;
    Ok(())
}

Binary Size Analysis for ESP32 (Actual Measurements)

Platform Binary Size Partition Used Status
ESP32-S3 (Snappy) 997 KB 24.73% Verified
ESP32-S3 (Uncompressed) 989 KB 24.52% ✅ Works
ESP32-S3 (ZSTD) N/A N/A ❌ Cross-compile fails

ESP32-S3 Flash Budget

Component Size
Total Flash 16 MB
App Partition 4 MB (default)
Parquet + S3 Binary ~1 MB
Remaining ~3 MB

Binary Breakdown (Estimated)

Component Size
ESP-IDF runtime ~500 KB
WiFi stack ~150 KB
TLS (mbedTLS) ~100 KB
Parquet crate ~200 KB
Snappy compression ~8 KB
rusty-s3 ~50 KB
Application code ~10 KB

Deployment Status: COMPLETE!

POC Status

Loading diagram...

The POC is working! See the full implementation:

What the POC Demonstrates

  1. Parquet Generation: Creates legitimate Parquet files with sensor schema directly on ESP32-S3
  2. Snappy Compression: Uses pure Rust Snappy (~7-8 KB for 178 rows)
  3. S3 Upload: Uploads via HTTP PUT with chunked transfer encoding (8KB chunks)
  4. AWS Signature V4: Generates presigned URLs on-device using rusty-s3
  5. Time Sync: SNTP for valid AWS signatures

S3 Client Options for ESP32

Comparison of Rust S3 Crates

Crate Approach Dependencies ESP32 Suitable Binary Impact
rusty-s3 Sans-IO Minimal (HMAC, SHA2) Best +250 KB
rust-s3 Full client Tokio/attohttpc Possible (sync) +500 KB
aws-sdk-s3 Official AWS Heavy (Tokio, Hyper) Too heavy +2 MB

Why rusty-s3 is perfect for ESP32:

  • Sans-IO approach: Only handles URL signing, you bring your own HTTP client
  • Minimal dependencies: Just HMAC-SHA256 for signing
  • Works with any HTTP client: Use esp-idf-svc on ESP32
  • Presigned URLs: Generate signed PUT URLs for direct S3 upload

Example: Presigned URL Generation

use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle};
use std::time::Duration;

fn generate_s3_upload_url(object_key: &str, credentials: &Credentials) -> String {
    let bucket = Bucket::new(
        "https://s3.us-west-2.amazonaws.com".parse().unwrap(),
        UrlStyle::VirtualHost,
        "your-bucket",
        "us-west-2",
    ).unwrap();

    let mut put_action = bucket.put_object(Some(credentials), object_key);
    put_action.headers_mut().insert("content-type", "application/octet-stream");

    // Sign URL - valid for 1 hour
    put_action.sign(Duration::from_secs(3600)).to_string()
}

ESP32 HTTP Upload with esp-idf-svc

use esp_idf_svc::http::client::{EspHttpConnection, Configuration};
use embedded_svc::http::client::Client;

fn upload_to_s3(signed_url: &str, parquet_data: &[u8]) -> Result<(), Error> {
    let config = Configuration::default();
    let mut client = Client::wrap(EspHttpConnection::new(&config)?);

    let headers = [("Content-Type", "application/octet-stream")];
    let mut request = client.put(signed_url, &headers)?;
    request.write_all(parquet_data)?;

    let response = request.submit()?;
    if response.status() == 200 {
        log::info!("Upload successful!");
    }
    Ok(())
}

Architecture for S3 Upload

Primary: Direct Parquet on ESP32 (PROVEN & WORKING!)

Primary Architecture

Loading diagram...

This architecture is PROVEN and WORKING! See POC Repository.

Fallback: Hybrid Architecture (If needed)

Hybrid Architecture

Loading diagram...

Data Flow Options

Option 1: Direct S3 Upload (Presigned URLs)

Direct S3 Upload Flow

Loading diagram...

Option 2: MQTT Bridge

MQTT Bridge Flow

Loading diagram...

Option 3: Direct HTTP to Processing Server

Direct HTTP Flow

Loading diagram...

Server-Side Parquet Conversion

Python Lambda Example

import boto3
import pyarrow as pa
import pyarrow.parquet as pq
import postcard  # or cbor2

def lambda_handler(event, context):
    # Download from S3
    s3 = boto3.client('s3')
    obj = s3.get_object(Bucket='raw-bucket', Key=event['key'])
    compressed_data = obj['Body'].read()

    # Deserialize (Postcard/CBOR/custom)
    readings = postcard.loads(compressed_data)

    # Convert to Arrow Table
    table = pa.Table.from_pydict({
        'timestamp': [r['timestamp'] for r in readings],
        'temperature': [r['temperature'] for r in readings],
        'humidity': [r['humidity'] for r in readings],
    })

    # Write as Parquet with compression
    pq.write_table(table, '/tmp/output.parquet', compression='snappy')

    # Upload to optimized bucket
    s3.upload_file('/tmp/output.parquet', 'parquet-bucket',
                   f'{event["key"]}.parquet')

Rust Lambda Example

use arrow::array::*;
use arrow::datatypes::*;
use parquet::arrow::ArrowWriter;

// Deserialize from Postcard/CBOR
let readings: Vec<SensorReading> = postcard::from_bytes(&data)?;

// Build Arrow arrays
let timestamps = UInt64Array::from(
    readings.iter().map(|r| r.timestamp).collect::<Vec<_>>()
);
let temps = Float32Array::from(
    readings.iter().map(|r| r.temperature).collect::<Vec<_>>()
);

// Create schema
let schema = Schema::new(vec![
    Field::new("timestamp", DataType::UInt64, false),
    Field::new("temperature", DataType::Float32, false),
]);

// Write Parquet
let file = File::create("output.parquet")?;
let mut writer = ArrowWriter::try_new(file, Arc::new(schema), None)?;
writer.write(&batch)?;
writer.close()?;

Format Pros Cons Best For
Postcard Tiny, no_std, fast Less common ESP32 primary choice
CBOR Standard, no_std Slightly larger Interoperability needed
Gorilla+Postcard Best compression More complex High-frequency sensors
JSON Human-readable Large, slow Debugging only
CSV Simple Very large Legacy systems

Development Setup

Toolchain Installation

# Install espup (manages ESP Rust toolchains)
cargo install espup

# Install toolchains for your target chip
espup install

# For ESP32-S3 (Xtensa) - installs fork automatically
# For ESP32-C6 (RISC-V) - uses standard toolchain

# Source the environment (add to .bashrc/.zshrc)
source ~/export-esp.sh

Project Generation

# Install project generator
cargo install esp-generate

# Generate new project
esp-generate --chip=esp32s3 my-sensor-project

# Or for ESP32-C6
esp-generate --chip=esp32c6 my-sensor-project

Flashing and Monitoring

# Install flash tool
cargo install espflash

# Build and flash
cd my-sensor-project
cargo build --release
espflash flash --monitor target/xtensa-esp32s3-espidf/release/my-sensor-project

Project Structure (std approach)

Essential Crates


Memory Considerations

ESP32-S3 Memory Map

ESP32-S3 Memory

Loading diagram...

Memory Budget for S3 Upload

Component Estimated Memory
WiFi stack ~50-100 KB
HTTP client ~20-30 KB
TLS (HTTPS) ~40-60 KB
Sensor data buffer Variable
Postcard/CBOR formatting ~5-10 KB
Available for data ~300 KB SRAM + 8MB PSRAM

ESP32-C6 Memory Constraints

ESP32-C6 Memory

Loading diagram...

With only 512KB and no PSRAM, you must:

  • Upload data more frequently
  • Use compact binary formats (Postcard/CBOR)
  • Minimize buffering

Sample Code Structure

WiFi Connection (std approach)

use esp_idf_svc::{
    wifi::{EspWifi, ClientConfiguration, Configuration},
    eventloop::EspSystemEventLoop,
    nvs::EspDefaultNvsPartition,
};

fn connect_wifi() -> Result<EspWifi<'static>, Error> {
    let sys_loop = EspSystemEventLoop::take()?;
    let nvs = EspDefaultNvsPartition::take()?;

    let mut wifi = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;

    wifi.set_configuration(&Configuration::Client(ClientConfiguration {
        ssid: "YourSSID".try_into().unwrap(),
        password: "YourPassword".try_into().unwrap(),
        ..Default::default()
    }))?;

    wifi.start()?;
    wifi.connect()?;

    Ok(wifi)
}

HTTP Upload to S3

use esp_idf_svc::http::client::{EspHttpConnection, Configuration};
use embedded_svc::http::client::Client;

fn upload_to_s3(presigned_url: &str, data: &[u8]) -> Result<(), Error> {
    let config = Configuration::default();
    let mut client = Client::wrap(EspHttpConnection::new(&config)?);

    let headers = [("Content-Type", "application/octet-stream")];

    let mut request = client.put(presigned_url, &headers)?;
    request.write_all(data)?;

    let response = request.submit()?;

    if response.status() == 200 {
        log::info!("Upload successful!");
    }

    Ok(())
}

Complete Sensor + Upload Example

use postcard::to_allocvec;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct SensorReading {
    timestamp: u64,
    temperature: f32,
    humidity: f32,
}

fn collect_and_upload(readings: Vec<SensorReading>, presigned_url: &str) -> Result<(), Error> {
    // Serialize with Postcard (tiny binary format)
    let data = to_allocvec(&readings)?;

    log::info!("Serialized {} readings to {} bytes", readings.len(), data.len());

    // Upload to S3
    upload_to_s3(presigned_url, &data)?;

    Ok(())
}

Final Recommendation

Final Recommendation

Loading diagram...

Summary

If You Need… Choose Why
Best overall for your project ESP32-S3 (8MB PSRAM) Maximum memory, native Parquet works!
Standard Rust toolchain ESP32-C6 No fork needed, 8MB flash sufficient
Future-proofing Wait for ESP32-C61 WiFi 6 + PSRAM, best of both worlds
Maximum power ESP32-P4 + C6 Only if you need video/AI processing

Key Takeaways (Updated December 2025 - POC VERIFIED!)

  1. Native Parquet on ESP32 IS WORKING! - POC verified on actual ESP32-S3 hardware
  2. Snappy compression is the answer - Pure Rust, cross-compiles perfectly, only +8 KB overhead
  3. ZSTD does NOT work - C library cross-compilation fails due to endianness issues
  4. rusty-s3 for S3 upload - Sans-IO, minimal deps, perfect for ESP32
  5. Binary size: 997 KB - Only 24.73% of partition used!
  6. Memory usage ~110 KB - fits easily in 8MB PSRAM
  7. ESP32-S3 with 8MB PSRAM is the best choice
  8. Direct ESP32 → Parquet → S3 architecture is PROVEN and WORKING!
  9. No Lambda/server-side conversion needed - Full end-to-end on ESP32

POC Repository: github.com/walkthru-earth/esp32s3-parquet-test


Resources

POC Repository

Official Documentation

Pure Rust Compression (ESP32 Compatible)

  • snap (Snappy) - Recommended - Pure Rust Snappy
  • lz4_flex - Pure Rust LZ4 (not integrated with parquet crate)
  • ruzstd - Pure Rust ZSTD decoder only (cannot compress)

S3 Clients

  • rusty-s3 - Sans-IO S3 client (recommended for ESP32)

Cross-Compilation Issues

Serialization Libraries

Parquet/Arrow

Community

Development Boards Database


Data Volume & Cost Analysis (Per Sensor)

Based on actual data from opensensor.space S3 bucket (station 019ab390-f291-7a30-bca8-381286e4c2aa).

Measured Data

Metric Value
Avg Parquet File Size 12.89 KB
Upload Frequency Every 15 minutes
Files per Day 96

Data Volume Per Sensor

Period Data Volume
Per Day 1.21 MB
Per Month 36.78 MB
Per Year 441 MB (0.43 GB)

AWS S3 Costs (us-west-2)

Cost Type Monthly Yearly
Storage $0.001 $0.06
PUT Requests (2,922/month) $0.015 $0.18
Total $0.02 $0.24

Scaling to Multiple Sensors

Sensors Data/Month Data/Year Cost/Year
1 37 MB 441 MB $0.24
10 368 MB 4.3 GB $2.35
100 3.6 GB 43 GB $23
1000 36 GB 431 GB $235

ESP32 WiFi Feasibility

  • Each upload: ~13 KB + HTTP overhead ≈ 15 KB per request
  • Daily bandwidth: ~1.4 MB per sensor
  • Monthly bandwidth: ~42 MB per sensor
  • Very feasible for ESP32 WiFi!

IoT Cellular Connectivity: SIM Card Providers

For remote sensor deployments without WiFi access, cellular connectivity via LTE-M or NB-IoT is essential.

Provider Comparison

IoT SIM Providers

Loading diagram...

Detailed Comparison

Provider SIM Cost Monthly Fee Data Cost Coverage Best For
1NCE $10 $0 Included (500MB/10yr) 170+ countries Long-term low-data IoT
Hologram $3-5 $1 (can pause) $0.03/MB 190+ countries Flexible/variable usage
Soracom $5 $0 $0.002/KB US (AT&T/T-Mo/VZW) High-volume US deployments

Why 1NCE is perfect for your use case:

Feature Value
Total Cost $10 one-time (covers 10 years!)
Data Included 500 MB over 10 years
Your Monthly Usage ~42 MB per sensor
Annual Usage ~441 MB per sensor
Coverage LTE-M/NB-IoT in 170+ countries
Networks (US) AT&T, T-Mobile

Calculation for your sensor:

  • Annual data: 441 MB
  • 10-year allowance: 500 MB
  • Result: 500 MB is tight for 10 years, but:
    • 1NCE offers top-up options
    • Consider WiFi for high-frequency deployments
    • Use for remote/cellular-only locations

Hologram

Feature Value
SIM Cost $3-5
Monthly Fee $1 (can pause when not in use)
Data Cost $0.03/MB
Your Monthly Cost ~$1.26/month (42 MB × $0.03)
Annual Cost ~$27/year per sensor
Coverage 190+ countries, multiple carriers per region

Pros:

  • Most flexible - pay only for what you use
  • Can pause SIM when not needed
  • Excellent dashboard and API

Cons:

  • Higher per-MB cost adds up for regular uploads

Soracom

Feature Value
SIM Cost $5
Plan Options plan-D ($0.002/KB), plan-US ($2/1GB)
Coverage US: AT&T, T-Mobile, Verizon
Best Price plan-US: $2/GB

Your monthly cost with plan-US:

  • 42 MB/month = $0.08/month
  • Annual: ~$1/year per sensor

Pros:

  • Cheapest for higher volumes
  • Strong US coverage (3 carriers)
  • Japanese company with excellent IoT focus

Cons:

  • US-focused (limited global coverage)
  • More complex pricing tiers

Cost Comparison for Your Use Case (42 MB/month)

Provider Year 1 Year 5 Year 10
1NCE $10 $10 $10
Hologram $27 $135 $270
Soracom (plan-US) $7 $11 $16

Recommendation

Cellular Connectivity Decision

Loading diagram...

For opensensor.space sensors:

  • Primary recommendation: 1NCE - $10 total for 10 years, perfect for low-bandwidth sensor data
  • Alternative (US): Soracom - Best rates for US deployments
  • Flexible option: Hologram - Best if usage varies or you need to pause SIMs

ESP32 Cellular Modules

For cellular connectivity, you'll need an ESP32 paired with an LTE-M/NB-IoT modem:

Module Description Price Range
LILYGO T-SIM7600G ESP32-S3 + SIM7600 (4G LTE) ~$40-50
LILYGO T-A7670 ESP32 + A7670 (4G LTE-Cat 1) ~$25-35
Waveshare SIM7080G Add-on for any ESP32 (LTE-M/NB-IoT) ~$20-30

These modules support the LTE-M/NB-IoT bands used by 1NCE, Hologram, and Soracom.


Last Updated: December 2025 - POC Verified on ESP32-S3 Hardware! Rust Toolchain: 1.91.1 (ESP) Tested on: ESP32-S3 with 16MB Flash, 8MB PSRAM