Understanding the difference between process.env and context.env in Cloudflare Workers and Pages Functions.
The Two Ways to Access Environment Variables
Cloudflare Workers/Pages Functions provide two ways to access environment variables:
| Method | Runtime | Available When | Requires Flag |
|---|---|---|---|
context.env | Native Workers | Inside request handler | No |
process.env | Node.js compat | Module load time + handler | Yes (nodejs_compat) |
Both read from the same source (Cloudflare dashboard or wrangler.toml), but differ in when your code can access them.
Availability Timeline
Worker Lifecycle
================
┌─────────────────────────────────────────────────────────────────┐
│ COLD START │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Module Load Time │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ const config = { ... }; // Runs here │ │
│ │ const client = new Client(); // Runs here │ │
│ │ │ │
│ │ process.env ✓ Available (with nodejs_compat) │ │
│ │ context.env ✗ NOT available │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
├─────────────────────────────────────────────────────────────────┤
│ REQUEST │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 2. Request Handler Time │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ export async function onRequest(context) { │ │
│ │ // Runs here, for each request │ │
│ │ │ │
│ │ process.env ✓ Available (with nodejs_compat) │ │
│ │ context.env ✓ Available │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Quick Comparison
| Phase | context.env | process.env (with nodejs_compat) |
|---|---|---|
| Module load (top-level code) | ✗ Not available | ✓ Available |
Inside onRequest handler | ✓ Available | ✓ Available |
| Third-party library code | ✗ Must be passed explicitly | ✓ Globally accessible |
Code Example
// ┌─────────────────────────────────────────────────────────────┐
// │ MODULE LOAD TIME - runs once on cold start │
// └─────────────────────────────────────────────────────────────┘
// ✗ FAILS - context doesn't exist yet
// const url = context.env.AUTH_URL; // ReferenceError: context is not defined
// ✓ WORKS with nodejs_compat
const url = process.env.AUTH_URL;
// ✗ FAILS without nodejs_compat
// const url = process.env.AUTH_URL; // ReferenceError: process is not defined
// ┌─────────────────────────────────────────────────────────────┐
// │ REQUEST TIME - runs for each request │
// └─────────────────────────────────────────────────────────────┘
export async function onRequest(context) {
// ✓ WORKS - context is passed by runtime
const url1 = context.env.AUTH_URL;
// ✓ WORKS with nodejs_compat
const url2 = process.env.AUTH_URL;
}context.env (Native Workers Pattern)
The native way to access environment variables in Cloudflare Workers:
export async function onRequest(context) {
const authUrl = context.env.AUTH_URL;
// Use authUrl...
}Key characteristics:
- Available only inside the request handler
- No compatibility flags needed
- The
context(orenvparameter) is passed to your handler by the runtime - Cannot be accessed at module load time
Example - Pages Function:
// functions/_middleware.ts
export async function onRequest(context) {
// context.env is available here
const secret = context.env.API_SECRET;
// Do something with it
return context.next();
}process.env (Node.js Compatibility)
The Node.js-style way, requiring the nodejs_compat flag:
// Available at module load time
const AUTH_URL = process.env.AUTH_URL;
export async function onRequest(context) {
// Also available here
const secret = process.env.API_SECRET;
}Key characteristics:
- Requires
nodejs_compatcompatibility flag - Available at module load time (global scope)
- Populated lazily on first access to
process - Matches Node.js patterns that libraries expect
Enabling nodejs_compat
In wrangler.toml:
compatibility_flags = ["nodejs_compat"]
compatibility_date = "2024-09-23"With compatibility date >= 2024-09-23, nodejs_compat_populate_process_env is automatically enabled, which populates process.env with your configured environment variables.
When to Use Each
Use context.env when:
- You can access the value inside the request handler
- You want to avoid adding compatibility flags
- You're writing new code without Node.js dependencies
Use process.env when:
- Third-party Node.js libraries read from
process.envinternally - You need the value at module load time with no way to defer
- You're porting Node.js code that expects
process.env
Common Mistake: Module Load Time Access
A common pattern that fails without nodejs_compat:
// This runs at module load time
const config = {
authUrl: process.env.AUTH_URL, // ERROR: process is not defined
};
export async function onRequest(context) {
// Too late - config was already created
}Solutions:
-
Use nodejs_compat (if you need module-level access):
// With nodejs_compat flag enabled const AUTH_URL = process.env.AUTH_URL; -
Defer to request time (native pattern):
export async function onRequest(context) { const authUrl = context.env.AUTH_URL; const config = { authUrl }; // ... } -
Lazy initialization:
let cachedConfig: Config | null = null; function getConfig(env: Env): Config { if (!cachedConfig) { cachedConfig = { authUrl: env.AUTH_URL }; } return cachedConfig; } export async function onRequest(context) { const config = getConfig(context.env); }
Security Considerations
context.env
- Scoped to the request handler
- Not globally accessible
- Third-party code in your bundle cannot access it unless you pass it
process.env (with nodejs_compat)
- Globally accessible from anywhere in your Worker
- Third-party libraries can read any value
- Consistent with Node.js behavior, but worth noting for security-sensitive values
From the Cloudflare docs:
Because
process.envis accessible at the global scope, it is important to note that environment variables are accessible from anywhere in your Worker script, including third-party libraries that you may be using.
Static Sites and Frontend Code
Neither context.env nor process.env leak to frontend code.
Pages Functions run server-side on Cloudflare's edge. Static assets (HTML/JS/CSS) are separate and never have access to these environment variables.
┌─────────────────────────────────────────────────────┐
│ Cloudflare Edge │
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Pages Functions │ │ Static Assets │ │
│ │ (server-side) │ │ (sent to browser) │ │
│ │ │ │ │ │
│ │ context.env ✓ │ │ No env access │ │
│ │ process.env ✓ │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────┘What DOES leak to frontend:
- Build-time variables baked into JS bundles (
NEXT_PUBLIC_*,VITE_*) - These are replaced by your bundler at build time, not runtime
Setting Environment Variables
Both context.env and process.env read from the same sources:
Cloudflare Dashboard
Pages > Your Project > Settings > Environment Variables
wrangler.toml
[vars]
AUTH_URL = "https://auth.example.com"
API_KEY = "secret-key".dev.vars (local development)
AUTH_URL=http://localhost:3000
API_KEY=dev-keynodejs_compat Trade-offs
Advantages
- Official Cloudflare solution, actively maintained
- Works with existing Node.js code and libraries
- Lazy loading minimizes overhead
- Growing API support (Buffer, crypto, AsyncLocalStorage, etc.)
Disadvantages
- Adds Node.js compatibility layer
- Some APIs are polyfilled (slightly larger bundle with
nodejs_compat_v2) - Must use
node:prefixed imports in some cases process.envglobally accessible (security consideration)
Practical Example: Auth Middleware
A middleware that needs an auth URL for JWT verification:
Approach 1: context.env (Recommended)
// functions/_middleware.ts
import { createTokenVerifier } from "@hikari/citizen/verify";
// JWKS cache is global, so creating verifier per-request is fine
const verifierCache = new Map<string, ReturnType<typeof createTokenVerifier>>();
function getVerifier(authUrl: string) {
let verifier = verifierCache.get(authUrl);
if (!verifier) {
verifier = createTokenVerifier(authUrl);
verifierCache.set(authUrl, verifier);
}
return verifier;
}
export async function onRequest(context) {
const authUrl = context.env.AUTH_URL;
if (!authUrl) {
throw new Error("AUTH_URL environment variable is required");
}
const verifyToken = getVerifier(authUrl);
// ... use verifyToken
}Approach 2: process.env (Requires nodejs_compat)
// functions/_middleware.ts
import { createAuthMiddleware } from "@hikari/citizen/edge";
const AUTH_URL = process.env.AUTH_URL;
if (!AUTH_URL) {
throw new Error("AUTH_URL environment variable is required");
}
export const onRequest = createAuthMiddleware({
authUrl: AUTH_URL,
protectedPaths: ["/private/"],
});