# How Bun Handles Environment Variables at Runtime: A Deep Dive into the Zig Implementation

> Discover how Bun handles environment variables at runtime. Explore the native OS integration, .env parsing, and unified JavaScript exposure via process.env Bun.env and import.meta.env.

- Repository: [Bun/bun](https://github.com/oven-sh/bun)
- Tags: deep-dive
- Published: 2026-02-28

---

**Bun manages environment variables through a three-stage pipeline written in Zig that copies the native OS environment, parses `.env` files with variable expansion, and exposes a unified, read-only Map to JavaScript via `process.env`, `Bun.env`, and `import.meta.env`.**

Bun's runtime treats environment variables as a first-class feature, implementing a deterministic, high-performance system that eliminates race conditions while supporting sophisticated `.env` file parsing. According to the `oven-sh/bun` source code, the entire system centers around a single `Map` instance defined in `src/env.zig` that serves as the immutable source of truth after startup.

## The Three-Stage Loading Pipeline

Bun initializes its environment in a strict sequence during process startup. This ensures predictable behavior across platforms and prevents conflicts between native OS variables and file-based configuration.

### Stage 1: Native Process Environment Loading

When Bun starts, `Loader.loadProcess()` in `src/env.zig` (lines 50-68) iterates over `std.os.environ` to copy every `KEY=VALUE` pair into an internal `Map`. The function splits each entry on the first `=` character, stores empty strings for keys without values, and sets a `did_load_process` flag to prevent re-initialization.

This stage creates the initial snapshot of the OS environment. Once complete, Bun never queries the OS environment again, ensuring consistent reads throughout the process lifetime.

### Stage 2: .env File Parsing and Precedence

After loading the native environment, `Loader.load()` in `src/env_loader.zig` (lines 94-108) handles `.env` file processing through two distinct paths:

**Automatic Loading** (unless `--no-env-file` is passed):
- `loadDefaultFiles()` walks a strict precedence order: `.env.local`, `.env.{development,production,test}` (based on `NODE_ENV` or `BUN_ENV`), then `.env`
- Later files overwrite earlier values using the `override=true` flag in `Map.put`

**Explicit Loading** (via `--env-file`):
- `loadExplicitFiles()` processes user-specified files in the order provided on the command line

**Parsing Implementation**:
The custom `Parser` in `src/env_loader.zig` supports unquoted, single-quoted, double-quoted, and back-ticked values. It handles **variable expansion** using `$VAR` or `${VAR}` syntax, including the `${VAR:-default}` fallback syntax for default values.

### Stage 3: Runtime JavaScript Exposure

Once startup completes, the `Map` becomes read-only and Bun exposes three identical interfaces:

- **`process.env`** – Node.js-compatible object
- **`Bun.env`** – Bun-specific global
- **`import.meta.env`** – ES Modules standard

These proxy reads to `Loader.get(key)`, which returns a nullable string. For boolean checks, `Loader.has(key)` implements truthiness validation that ignores empty strings, `"0"`, and `"false"`.

Special-purpose getters like `Loader.getTLSRejectUnauthorized()` cache the boolean interpretation of `NODE_TLS_REJECT_UNAUTHORIZED` for Node.js compatibility.

## Low-Level Platform Integration

The `Map` structure in `src/env.zig` provides platform-specific utilities for spawning child processes:

- **`Map.createNullDelimitedEnvMap`** builds a C-style `char **envp` array for POSIX `execve` calls
- **`Map.writeWindowsEnvBlock`** creates a UTF-16 environment block for Windows `CreateProcessW`

On Windows, Bun uses `bun.CaseInsensitiveASCIIStringArrayHashMap` for the `Map` implementation, matching Node.js behavior where `process.env.Path` and `process.env.PATH` resolve identically.

## Working with Bun Environment Variables

Bun supports both reading and writing environment variables, though with different semantics:

```typescript
// Reading variables (returns string | undefined)
const apiKey = process.env.API_KEY;
const port = Bun.env.PORT ?? "3000";

// Writing variables updates the internal Map and OS env for child processes
process.env.DEBUG = "true";

// Boolean checking with truthiness validation
if (Bun.env.has("NODE_TLS_REJECT_UNAUTHORIZED")) {
  // Only true if value is not "", "0", or "false"
}

```

```zig
// Internal Zig access (src/env_loader.zig)
var envMap = Loader.init(allocator);
envMap.loadProcess();          // Copy OS environment
envMap.load();                 // Load .env files with precedence
const home = envMap.map.get("HOME"); // Returns ?[]const u8

```

## Summary

- **Single Source of Truth**: All environment values live in one `Map` instance initialized during startup; the OS environment is never queried again after `Loader.loadProcess()` completes.
- **Thread-Safe by Design**: The `Map` is only mutated during the startup phase, eliminating race conditions during runtime JavaScript execution.
- **Platform-Consistent**: Windows uses case-insensitive keys via `CaseInsensitiveASCIIStringArrayHashMap`, while POSIX systems respect case sensitivity.
- **Variable Expansion**: The custom `Parser` supports `$VAR`, `${VAR}`, and `${VAR:-default}` syntax within `.env` files.
- **Child Process Ready**: The `Map` provides `createNullDelimitedEnvMap` and `writeWindowsEnvBlock` for correctly passing the current environment to spawned processes.

## Frequently Asked Questions

### How does Bun determine which `.env` files to load automatically?

Bun follows a strict precedence order unless `--no-env-file` is specified. The loader checks `NODE_ENV` or `BUN_ENV` to select environment-specific files, loading `.env.local` first, then `.env.{development,production,test}`, and finally `.env`. Later files overwrite earlier values, as implemented in `loadDefaultFiles()` within `src/env_loader.zig`.

### Is `process.env` case-sensitive on Windows?

No. On Windows, Bun uses `bun.CaseInsensitiveASCIIStringArrayHashMap` for the internal environment `Map`, making `process.env.Path` and `process.env.PATH` return the same value. This matches Node.js behavior. On Linux and macOS, environment variables remain case-sensitive.

### Does Bun support variable expansion inside `.env` files?

Yes. The `Parser` in `src/env_loader.zig` supports shell-style variable expansion including `$VAR`, `${VAR}`, and the `${VAR:-default}` syntax for default values. This expansion occurs during the loading phase before values are stored in the internal `Map`.

### How does Bun handle environment variable modifications for child processes?

When you assign to `process.env.KEY`, Bun updates the internal `Map` and ensures these changes propagate to child processes through platform-specific APIs. The `Map.createNullDelimitedEnvMap` function builds POSIX-compatible `envp` arrays, while `Map.writeWindowsEnvBlock` creates the UTF-16 environment blocks required by Windows `CreateProcessW`.