R - Read/Write RDS and RData Files

R provides two native binary formats for persisting objects: RDS and RData. RDS files store a single R object, while RData files can store multiple objects from your workspace. Both formats preserve...

Key Insights

  • RDS files store single R objects with full type preservation, making them ideal for saving models, data frames, or complex structures between sessions without losing metadata or attributes
  • RData files can save multiple objects in one file but require knowing object names when loading, while RDS files return the object directly for assignment to any variable name
  • Compression options (gzip, bzip2, xz) can reduce file sizes by 80-90% for large datasets with minimal performance impact, crucial for production data pipelines

Understanding RDS vs RData File Formats

R provides two native binary formats for persisting objects: RDS and RData. RDS files store a single R object, while RData files can store multiple objects from your workspace. Both formats preserve R’s complex data structures including attributes, classes, and metadata that CSV or text formats would lose.

# Create sample data
customer_data <- data.frame(
  id = 1:1000,
  revenue = rnorm(1000, 50000, 15000),
  segment = sample(c("Enterprise", "SMB", "Startup"), 1000, replace = TRUE)
)

# Add custom attributes
attr(customer_data, "created_date") <- Sys.Date()
attr(customer_data, "version") <- "1.0"

# Save as RDS
saveRDS(customer_data, "customer_data.rds")

# Save as RData
save(customer_data, file = "customer_data.RData")

The critical difference emerges when loading. RDS returns the object for assignment, while RData loads objects with their original names:

# RDS: assign to any variable name
customers <- readRDS("customer_data.rds")
client_info <- readRDS("customer_data.rds")  # Different name, same data

# RData: restores original object name
load("customer_data.RData")  # Creates 'customer_data' variable
# You cannot directly assign load() output to a new name

Saving and Loading Single Objects with RDS

RDS files excel when working with single objects like trained models, processed datasets, or configuration lists. The format preserves all object attributes and classes, essential for machine learning workflows.

library(randomForest)

# Train a model
set.seed(123)
model <- randomForest(
  segment ~ revenue + id,
  data = customer_data,
  ntree = 500
)

# Save model with compression
saveRDS(model, "customer_segment_model.rds", compress = "xz")

# Check file size
file.info("customer_segment_model.rds")$size / 1024  # Size in KB

# Load in new session
loaded_model <- readRDS("customer_segment_model.rds")

# Verify model integrity
identical(model$confusion, loaded_model$confusion)

For production pipelines, use compression strategically. The compress parameter accepts “gzip” (default), “bzip2”, or “xz”:

# Compare compression methods
test_data <- matrix(rnorm(1000000), ncol = 100)

saveRDS(test_data, "test_gzip.rds", compress = "gzip")
saveRDS(test_data, "test_bzip2.rds", compress = "bzip2")
saveRDS(test_data, "test_xz.rds", compress = "xz")
saveRDS(test_data, "test_none.rds", compress = FALSE)

# Compare sizes
data.frame(
  method = c("gzip", "bzip2", "xz", "none"),
  size_mb = c(
    file.size("test_gzip.rds"),
    file.size("test_bzip2.rds"),
    file.size("test_xz.rds"),
    file.size("test_none.rds")
  ) / 1024^2
)

Working with Multiple Objects in RData Files

RData files save entire workspaces or selected objects. This proves useful for checkpointing analysis sessions or bundling related objects.

# Create multiple related objects
customer_summary <- aggregate(revenue ~ segment, customer_data, mean)
total_customers <- nrow(customer_data)
analysis_date <- Sys.Date()

# Save multiple objects
save(customer_data, customer_summary, total_customers, analysis_date,
     file = "customer_analysis.RData")

# Save entire workspace
save.image("workspace_backup.RData")

# Load specific file
load("customer_analysis.RData")

# List objects in RData file without loading
load("customer_analysis.RData", envir = test_env <- new.env())
ls(test_env)

When loading RData files in production code, control the environment to avoid polluting the global namespace:

# Load into isolated environment
analysis_env <- new.env()
load("customer_analysis.RData", envir = analysis_env)

# Access objects explicitly
summary_data <- analysis_env$customer_summary
total <- analysis_env$total_customers

# Clean up
rm(analysis_env)

Performance Optimization for Large Files

For datasets exceeding 1GB, compression and read/write performance become critical. Benchmark different approaches:

# Create large dataset
large_data <- data.frame(
  matrix(rnorm(10000000), ncol = 100)
)

# Benchmark write performance
library(microbenchmark)

write_benchmark <- microbenchmark(
  rds_gzip = saveRDS(large_data, "temp_gzip.rds", compress = "gzip"),
  rds_xz = saveRDS(large_data, "temp_xz.rds", compress = "xz"),
  rds_none = saveRDS(large_data, "temp_none.rds", compress = FALSE),
  times = 5
)

print(write_benchmark)

# Benchmark read performance
read_benchmark <- microbenchmark(
  read_gzip = readRDS("temp_gzip.rds"),
  read_xz = readRDS("temp_xz.rds"),
  read_none = readRDS("temp_none.rds"),
  times = 10
)

print(read_benchmark)

For extremely large objects, use the refhook parameter to handle reference objects properly:

# Create object with external pointers (e.g., database connections)
complex_object <- list(
  data = customer_data,
  connection_string = "postgresql://localhost/customers",
  timestamp = Sys.time()
)

# Custom serialization hook
saveRDS(complex_object, "complex.rds", refhook = function(x) {
  # Handle special object types
  if (inherits(x, "connection")) {
    return(NULL)
  }
  x
})

Production Patterns and Error Handling

Implement robust error handling and validation when working with RDS/RData files in production:

safe_read_rds <- function(filepath, default = NULL) {
  tryCatch({
    if (!file.exists(filepath)) {
      warning(sprintf("File not found: %s", filepath))
      return(default)
    }
    
    # Verify file integrity
    obj <- readRDS(filepath)
    
    # Validate object structure
    if (is.null(obj)) {
      warning(sprintf("NULL object in file: %s", filepath))
      return(default)
    }
    
    return(obj)
  }, error = function(e) {
    warning(sprintf("Error reading %s: %s", filepath, e$message))
    return(default)
  })
}

# Usage
model <- safe_read_rds("customer_segment_model.rds", default = NULL)
if (is.null(model)) {
  # Fallback logic
  stop("Failed to load required model")
}

Version your data files to track changes over time:

save_versioned_rds <- function(object, base_name) {
  timestamp <- format(Sys.time(), "%Y%m%d_%H%M%S")
  filename <- sprintf("%s_%s.rds", base_name, timestamp)
  saveRDS(object, filename, compress = "xz")
  
  # Also save as "latest"
  latest_filename <- sprintf("%s_latest.rds", base_name)
  saveRDS(object, latest_filename, compress = "xz")
  
  return(filename)
}

# Save with versioning
saved_file <- save_versioned_rds(customer_data, "customer_data")

Interoperability and Migration Strategies

When migrating between R versions or sharing data with other tools, consider compatibility:

# Check R version used to create file
rds_info <- function(filepath) {
  # Read raw bytes to check version
  con <- file(filepath, "rb")
  on.exit(close(con))
  
  # RDS files start with specific bytes
  header <- readBin(con, "raw", n = 20)
  
  list(
    file = filepath,
    size_mb = file.size(filepath) / 1024^2,
    exists = file.exists(filepath),
    modified = file.mtime(filepath)
  )
}

# Export to alternative formats for interoperability
export_rds_to_formats <- function(rds_file, base_name) {
  obj <- readRDS(rds_file)
  
  # Export to CSV (if data frame)
  if (is.data.frame(obj)) {
    write.csv(obj, paste0(base_name, ".csv"), row.names = FALSE)
  }
  
  # Export to Parquet (if arrow package available)
  if (requireNamespace("arrow", quietly = TRUE)) {
    arrow::write_parquet(obj, paste0(base_name, ".parquet"))
  }
  
  # Export to Feather (fast, language-agnostic)
  if (requireNamespace("feather", quietly = TRUE)) {
    feather::write_feather(obj, paste0(base_name, ".feather"))
  }
}

RDS and RData files remain the most efficient formats for R-native workflows, preserving all object characteristics while offering excellent compression and performance for production data pipelines.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.