Knowledge Base

Environment Variables in Cloudflare Workers and Pages Functions

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:

MethodRuntimeAvailable WhenRequires Flag
context.envNative WorkersInside request handlerNo
process.envNode.js compatModule load time + handlerYes (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

Phasecontext.envprocess.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 (or env parameter) 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_compat compatibility 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.env internally
  • 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:

  1. Use nodejs_compat (if you need module-level access):

    // With nodejs_compat flag enabled
    const AUTH_URL = process.env.AUTH_URL;
  2. Defer to request time (native pattern):

    export async function onRequest(context) {
      const authUrl = context.env.AUTH_URL;
      const config = { authUrl };
      // ...
    }
  3. 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.env is 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-key

nodejs_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.env globally 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/"],
});

Sources