coursera_2026_06
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

How to Migrate Express/Koa to Cloudflare Workers

  1. Set compatibility_date to the minimum date enabling node:http and add nodejs_compat_v2 to compatibility_flags in wrangler.toml.
  2. Separate your app.listen() call into a dedicated server.js file used only for local development.
  3. Create a worker.js entry point that wraps your Express app (or Koa's app.callback()) with httpServerHandler from cloudflare:workers.
  4. Audit all middleware against Workers compatibility—remove filesystem-dependent packages and replace in-memory session stores with KV or Durable Objects.
  5. Replace native addon dependencies (e.g., bcrypt, sharp) with pure-JS or WASM alternatives.
  6. Validate with wrangler deploy --dry-run for static import failures and wrangler dev --remote for runtime errors.
  7. Optimize bundle size via tree-shaking and bypass Express/Koa entirely on hot paths by returning native Response objects.
  8. Deploy to a staging environment, monitor error rates and P99 latency for 48 hours, then gradually cut over production traffic.

Running Express and Koa applications on Cloudflare Workers became practical in 2025 when Cloudflare introduced nodejs_compat_v2 with runtime-level node:http, node:net, and node:tls support. Previously, migration required userland polyfills for HTTP handling, manual request/response shimming, and third-party compatibility layers. nodejs_compat_v2 eliminates all three. But the path from a running Node.js server to a deployed Worker is full of subtle breakage points, from silently failing imports to middleware that assumes a persistent filesystem. This guide covers the end-to-end migration process with working code, a compatibility matrix for popular middleware, and concrete debugging strategies for the failures that documentation does not yet cover in one place.

Table of Contents

Why Migrate Express and Koa to Cloudflare Workers?

The Edge Deployment Advantage

Cloudflare Workers execute on a global network spanning over 300 cities. For latency-sensitive APIs, this eliminates the round-trip penalty of routing requests to a centralized origin server. A user in São Paulo hitting an API deployed on Workers gets a response from a nearby data center rather than crossing the Atlantic to reach a server in Frankfurt.

The cost model also differs fundamentally. Traditional deployments require always-on infrastructure, whether through reserved instances, containers, or managed services, all billed by uptime. Workers operate on per-request billing, meaning idle applications cost nothing. For bursty workloads or APIs with uneven traffic, compare your current monthly infrastructure bill against Cloudflare's Workers pricing calculator to determine whether per-request billing reduces costs for your request volume.

Workers use V8 isolates rather than containers. Per Cloudflare's published benchmarks, pre-warmed isolate startup times run under 5 ms, compared to the hundreds of milliseconds typical of serverless container platforms. Initial isolate creation on a new point of presence adds 10-50 ms per Cloudflare's documentation.

What Changed in 2025: nodejs_compat_v2

Cloudflare's Node.js compatibility journey began with limited polyfills for modules like node:buffer and node:crypto. These polyfills supported basic utilities but could not run HTTP frameworks, which depend on node:http for request and response handling.

nodejs_compat_v2 provides runtime-level node:http instead of userland polyfills. Rather than shimming individual APIs, Cloudflare now ships near-native implementations of core Node.js modules within the Workers runtime. node:http is required because Express and Koa both depend on it to create and manage HTTP server abstractions. Without it, neither framework can function.

nodejs_compat_v2 provides runtime-level node:http instead of userland polyfills. Rather than shimming individual APIs, Cloudflare now ships near-native implementations of core Node.js modules within the Workers runtime.

Prerequisites and Compatibility Date Requirements

Required Tooling and Versions

Before starting, verify you have:

  • Wrangler CLI 3.x+ (run wrangler --version; earlier versions do not support nodejs_compat_v2). Check the Cloudflare Workers changelog for the minimum 3.x version that introduced this flag.
  • Node.js 18+ in your local development environment for feature parity during testing.
  • A Cloudflare Workers Paid plan account. The Free plan imposes a 10ms CPU time limit per invocation, which is insufficient for most Express or Koa middleware stacks. The Paid plan's Bundled model allows up to 50ms CPU time.

For middleware-heavy applications, use the Unbound usage model. It bills by CPU time consumed rather than enforcing a hard cap.

Understanding Compatibility Dates

Compatibility dates in Cloudflare Workers function as versioned feature gates. Setting the wrong date does not throw an error at deploy time. Instead, imports for modules not yet available at the specified date will silently fail or produce cryptic runtime errors.

Consult the Cloudflare Workers compatibility dates changelog for the exact date that enables node:http, node:net, and node:tls. Verify this value before setting it in production. A later compatibility date adds read-only node:fs support, which can be useful for configuration file loading but does not support write operations. Check the same changelog for the specific date that enables this feature.

# wrangler.toml — minimum viable configuration for Express/Koa migration
name = "my-express-worker"
main = "src/worker.js"

# Set this to the minimum compatibility date that enables node:http, node:net, node:tls.
# Consult: https://developers.cloudflare.com/workers/configuration/compatibility-dates/
compatibility_date = "2025-08-15"
compatibility_flags = ["nodejs_compat_v2"]

[vars]
ENVIRONMENT = "production"

[[kv_namespaces]]
binding = "SESSION_KV"
# REQUIRED: replace before deploy.
# Run 'wrangler kv:namespace create SESSION_KV' and paste the resulting id here.
id = "your-kv-namespace-id"

[[r2_buckets]]
binding = "UPLOADS_R2"
bucket_name = "my-uploads-bucket"

Setting the compatibility date to anything before the minimum required date will cause node:http imports to fail at runtime, and the error message may not clearly indicate that the date is the problem.

Migrating an Express Application Step by Step

The httpServerHandler Pattern

Workers use a fetch event handler as the entry point for incoming requests. Express, on the other hand, expects to bind to a port and receive requests through Node.js's http.Server. The httpServerHandler function from the cloudflare:workers module bridges this gap. It accepts a Node.js HTTP request handler ((req, res) => void), creates a synthetic node:http server context, and feeds each incoming Worker request through it. Express applications are themselves Node.js HTTP request handlers, so they can be passed directly. The flow is: incoming request hits the Worker's fetch handler, which delegates to httpServerHandler, which invokes the Express app, which processes the request through its middleware and routing stack and returns a response.

Note: Verify the httpServerHandler API name and import path against the current Cloudflare Workers API reference before deploying, as this API may be updated across Wrangler versions.

Here is a conventional Express application before migration. Note that the application module should use ESM syntax (import/export) to match the Worker entry point, or your bundler must be configured for CJS-to-ESM interop.

// src/app.js — traditional Express app (before migration, converted to ESM)
import express from "express";

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/api/health", (req, res) => {
  res.json({ status: "ok", timestamp: Date.now() });
});

app.get("/api/users/:id", (req, res) => {
  const { id } = req.params;
  res.json({ id, name: `User ${id}`, source: "edge" });
});

app.post("/api/users", (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: "name and email required" });
  }
  res.status(201).json({ id: crypto.randomUUID(), name, email });
});

app.get("/api/config", (req, res) => {
  res.json({ region: "auto", version: "1.0.0" });
});

export default app;

If your original app.js uses CommonJS syntax (require, module.exports), either convert it to ESM (replace require with import, module.exports with export default) or configure your bundler (esbuild/Wrangler's built-in bundler) for CJS-to-ESM interop. Mixing require() in app.js with import in worker.js without bundler configuration will produce a module format error.

Create a separate server.js file for local Node.js development. This file is never imported by worker.js:

// src/server.js — local development only, NOT used by Workers
import app from "./app.js";

app.listen(3000, () => console.log("Server running on port 3000"));

Wrapping Express with httpServerHandler

The migration entry point replaces the app.listen() call with an httpServerHandler wrapper. The env and ctx objects from the Workers runtime are attached to the request so that route handlers can access bindings like KV namespaces and R2 buckets.

// src/worker.js — migrated Express entry point for Cloudflare Workers
import { httpServerHandler } from "cloudflare:workers";
import app from "./app.js";

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Fast path — bypass Express entirely for simple health checks
    if (url.pathname === "/ping") {
      return new Response("pong", { status: 200 });
    }

    // Per-request Workers bindings injected via the handler closure.
    // req.cloudflare is set immediately before app() is called,
    // so all downstream middleware sees it.
    return httpServerHandler((req, res) => {
      req.cloudflare = { env, ctx };
      app(req, res);
    })(request, env, ctx);
  },
};

The most reliable way to prevent app.listen() from running in a Workers context is to separate server startup into a dedicated server.js file that is never imported by worker.js, as shown above. Alternatively, you can check typeof globalThis.caches !== 'undefined' as a Workers-specific runtime signal if you need a single-file guard.

Handling Express Middleware in Workers

Not all middleware survives migration. Body parsing middleware like express.json() and express.urlencoded() works as expected because it operates on request streams, which are supported. Static file serving with express.static() will fail because Workers have no persistent local filesystem.

Session middleware presents a harder problem. Libraries like express-session default to in-memory storage, which is incompatible with the stateless nature of Workers isolates. Each request may execute in a different isolate, so in-memory session state is lost between requests.

// src/middleware.js — middleware stack before and after migration

// ---- BEFORE (Node.js server) ----
// const express = require("express");
// const session = require("express-session");
// const morgan = require("morgan");
// const cors = require("cors");

// app.use(express.static("public"));          // REMOVE — no filesystem
// app.use(morgan("combined"));                // KEEP — stdout logging works
// app.use(session({                           // REPLACE — in-memory store won't persist
//   secret: process.env.SESSION_SECRET,       // Use: wrangler secret put SESSION_SECRET
// }));

// ---- AFTER (Workers-compatible) ----
app.use(express.json());                        // ✅ Works unchanged
app.use(express.urlencoded({ extended: true })); // ✅ Works unchanged
app.use(cors());                                // ✅ Works unchanged
app.use(morgan("tiny"));                        // ⚠️ Works for stdout only

// Replace express-session with KV-backed sessions
const SESSION_PREFIX = "session:";
const SESSION_TTL = 86400;
const UUID_REGEX =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

app.use(async (req, res, next) => {
  try {
    const sessionId = req.headers["x-session-id"];

    if (sessionId && req.cloudflare) {
      // Validate session ID format before using as KV key
      if (!UUID_REGEX.test(sessionId)) {
        return res.status(400).json({ error: "Invalid session ID format" });
      }
      const data = await req.cloudflare.env.SESSION_KV.get(
        `${SESSION_PREFIX}${sessionId}`,
        "json"
      );
      req.session = data || {};
      req.sessionId = sessionId;
    } else {
      req.session = {};
      req.sessionId = crypto.randomUUID();
    }

    // Register KV session write with ctx.waitUntil to ensure it completes
    // before the isolate terminates. Do NOT use res.on("finish") with async
    // operations in Workers — the async callback is not guaranteed to complete.
    // Guard against double-patching by multiple middleware registrations.
    if (!res._sessionEndPatched && req.cloudflare) {
      res._sessionEndPatched = true;
      const originalEnd = res.end.bind(res);
      res.end = function (...args) {
        req.cloudflare.ctx.waitUntil(
          req.cloudflare.env.SESSION_KV.put(
            `${SESSION_PREFIX}${req.sessionId}`,
            JSON.stringify(req.session),
            { expirationTtl: SESSION_TTL }
          )
        );
        return originalEnd(...args);
      };
    }

    next();
  } catch (err) {
    next(err); // Propagate to Express error handler — never swallow
  }
});

⚠️ Important: Do not use res.on("finish", async () => { ... }) for KV writes in Workers. The async callback is not awaited by the runtime, and the isolate may terminate before the write completes. Always use ctx.waitUntil() to keep the isolate alive until background work finishes.

Migrating a Koa Application Step by Step

Koa's Compatibility Advantages

Koa's core does not import node:http directly; it delegates HTTP handling to app.callback(), which returns a standard (req, res) handler. Koa also does not bundle a router, a body parser, or a static file server. This minimal footprint means the base Koa package introduces fewer compatibility risks. Its async/await-first middleware model translates cleanly to the Workers environment, where asynchronous patterns are native.

Wrapping Koa with httpServerHandler

The wiring for Koa is nearly identical to Express, with one key difference: Koa applications are not themselves Node.js HTTP request handlers. Koa applications expose a callback() method that returns a standard Node.js HTTP handler ((req, res) => void). This is what gets passed to httpServerHandler, which expects a handler with that signature in both cases.

// src/worker-koa.js — complete Koa migration entry point
// Bindings are passed via closure — no module-level mutable globals.
import { httpServerHandler } from "cloudflare:workers";
import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "@koa/bodyparser"; // Note: koa-body is deprecated; use @koa/bodyparser (v5+)

function buildApp(env, ctx) {
  const app = new Koa();
  const router = new Router();

  // Error handling middleware
  app.use(async (koaCtx, next) => {
    try {
      await next();
    } catch (err) {
      koaCtx.status = err.status || 500;
      koaCtx.body = { error: err.message };
    }
  });

  // Request logging middleware
  app.use(async (koaCtx, next) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    console.log(
      JSON.stringify({
        method: koaCtx.method,
        url: koaCtx.url,
        status: koaCtx.status,
        ms,
      })
    );
  });

  app.use(bodyParser());

  // Per-request Workers bindings middleware — uses ctx.state for request isolation.
  // Bindings are captured via closure from buildApp arguments — no shared mutable state.
  app.use(async (koaCtx, next) => {
    koaCtx.state.cloudflare = { env, ctx };
    await next();
  });

  router.get("/api/health", (koaCtx) => {
    koaCtx.body = { status: "ok", runtime: "cloudflare-workers" };
  });

  router.post("/api/data", (koaCtx) => {
    const { payload } = koaCtx.request.body;
    if (payload === undefined || payload === null) {
      koaCtx.status = 400;
      koaCtx.body = { error: "payload is required" };
      return;
    }
    koaCtx.status = 201;
    koaCtx.body = { id: crypto.randomUUID(), payload };
  });

  app.use(router.routes());
  app.use(router.allowedMethods());

  return app;
}

export default {
  async fetch(request, env, ctx) {
    // App is constructed per-request with bindings captured in closure.
    // This is intentionally per-request to avoid shared mutable state.
    // If construction cost becomes measurable, consider Durable Objects
    // or a WeakMap keyed on env for caching.
    const app = buildApp(env, ctx);
    return httpServerHandler(app.callback())(request, env, ctx);
  },
};

Koa Middleware Compatibility Notes

JSON and URL-encoded parsing via @koa/bodyparser works unchanged on Workers. (koa-body is deprecated and should not be used for new projects.) Static file serving with koa-static fails for the same filesystem reason as express.static(). For sessions, koa-session requires the same KV or Durable Objects backing store approach described in the Express section, since the default in-memory or cookie-only stores either lose state or exceed cookie size limits for non-trivial session data.

Middleware Compatibility Matrix

Middleware Package Status Notes Alternative for Workers
express.json() ✅ Works Parses JSON request bodies
express.urlencoded() ✅ Works Parses URL-encoded bodies
express.static() ❌ Fails No local filesystem access Workers Assets / R2
cors ✅ Works Standard header manipulation
helmet ⚠️ Partial Some CSP/HSTS headers are irrelevant at the edge Review per-header
express-session ❌ Fails Requires persistent server-side store KV or Durable Objects
passport ⚠️ Partial Local strategies work; OAuth callbacks need URL adjustments Strategy-dependent
multer ❌ Fails Depends on node:fs write operations R2 direct upload
morgan ⚠️ Partial Console/stdout transport works; file transport fails Workers Logpush
compression ⚠️ Remove with care Cloudflare applies automatic compression to cached responses; for dynamic Worker responses, verify compression via response headers before removing this middleware Verify behavior per-route
cookie-parser ✅ Works Header-only operation
koa-router / @koa/router ✅ Works No native dependencies
@koa/bodyparser ✅ Works JSON and URL-encoded parsing; replaces deprecated koa-body
koa-static ❌ Fails No filesystem Workers Assets / R2
koa-session ❌ Fails Needs external store KV or Durable Objects
express-rate-limit ⚠️ Partial In-memory store resets per isolate Durable Objects counter
serve-favicon ❌ Fails Filesystem dependency Workers Assets

Debugging Failed Imports and Runtime Errors

Identifying Unsupported Node.js APIs

Two distinct failure modes exist. A No such module "node:xyz" error at startup means the module is entirely unavailable at the configured compatibility date. A runtime TypeError on a specific method call means the module is loaded but the particular API surface is not fully implemented. The second type is harder to diagnose because it only triggers when the code path executes.

A No such module "node:xyz" error at startup means the module is entirely unavailable at the configured compatibility date. A runtime TypeError on a specific method call means the module is loaded but the particular API surface is not fully implemented.

Tracing which dependency introduces an unsupported import requires following the dependency tree. A top-level library might work, but one of its transitive dependencies might pull in node:child_process or attempt filesystem writes.

// scripts/check-imports.mjs — diagnostic wrapper for problematic dependencies
// ⚠️ This script runs in Node.js and will report ALL node: modules as available.
// It is useful only to confirm your LOCAL Node.js version supports these modules.
// For Workers runtime availability, use: wrangler deploy --dry-run
// For runtime import failures, use: wrangler dev --remote and trigger each route.

// Note: Use .mjs extension or add "type": "module" to package.json for top-level await
const problematicModules = [
  "node:fs", "node:child_process", "node:cluster",
  "node:dgram", "node:readline", "node:vm"
];

console.warn(
  "NOTE: Results below reflect Node.js availability, NOT Cloudflare Workers availability."
);

for (const mod of problematicModules) {
  try {
    await import(mod);
    console.log(`[node.js] ✅ ${mod} — available in Node.js (may not be available in Workers)`);
  } catch (e) {
    console.log(`[node.js] ❌ ${mod}${e.message}`);
  }
}

console.log("
To check Workers compatibility, run:");
console.log("  npx wrangler deploy --dry-run --outdir=dist");
// package.json — add diagnostic scripts
{
  "scripts": {
    "check:deploy": "npx wrangler deploy --dry-run --outdir=dist",
    "dev:debug": "npx wrangler dev --log-level=debug"
  }
}

wrangler deploy --dry-run catches statically bundled import failures. Dynamic or conditional imports are only detectable at execution time; use wrangler dev --remote for runtime validation. The --log-level=debug flag on wrangler dev outputs detailed module resolution logs that show exactly which import statement fails.

Polyfilling vs. Replacing Dependencies

The unenv project provides aliases that stub unsupported Node.js APIs with no-op implementations or lightweight shims. This approach works when the unsupported API is used in a non-critical code path, such as a logging library that optionally writes to a file but falls back to stdout. When the unsupported API is central to the dependency's purpose, like bcrypt relying on native C++ addons, replace the dependency entirely. Swap bcrypt for bcryptjs (pure JavaScript) or the Web Crypto API. Note that bcryptjs is significantly slower than native bcrypt at equivalent work factors (3-10x); for latency-sensitive auth endpoints, consider using the Web Crypto API's SubtleCrypto.deriveKey with PBKDF2 as an alternative. When using bcryptjs on Workers, keep the work factor at 8 or below to stay within Workers CPU time limits (50ms Bundled). Image processing with sharp requires offloading to an external service or using a WASM-based alternative.

Common Migration Failures and Fixes

Workers do not support node:fs write operations. File uploads handled by multer should be redirected to R2 using the R2 binding. node:child_process has no equivalent and cannot be polyfilled; any functionality depending on spawning processes must be restructured, potentially using Service Bindings to call other Workers. Native C++ addons will not load in the V8 isolate environment. This is a hard constraint that cannot be worked around without switching to pure JavaScript or WASM implementations.

Performance Optimization for Edge Deployment

Bundle Size and Cold Start Reduction

Express and Koa both carry dependencies that are unnecessary in a Workers context. Tree-shaking during the build step can eliminate dead code, but many Express plugins are structured in ways that resist tree-shaking. Explicitly importing only the modules needed, rather than using barrel imports, helps. Lazy-loading route handlers reduces the initial parse time. Consult Cloudflare's Workers limits documentation for the current compressed bundle size cap. Larger bundles correlate with longer cold start times, so aim to stay well under the hard limit.

Using Workers-Native Features Alongside Express/Koa

Express and Koa route handlers can access Workers bindings directly through the request context. This allows blending familiar framework patterns with edge-native capabilities like KV, R2, and the Cache API.

// Express route using Workers KV and R2 bindings
const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5MB — tune to your use case
const KEY_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;

app.get("/api/documents/:key", async (req, res, next) => {
  try {
    const { env } = req.cloudflare;
    const { key } = req.params;

    // Validate key to prevent path traversal and injection
    if (!KEY_PATTERN.test(key)) {
      return res.status(400).json({ error: "Invalid document key" });
    }

    // Check KV for cached metadata
    const metadata = await env.SESSION_KV.get(`doc:${key}`, "json");
    if (!metadata) {
      return res.status(404).json({ error: "Document not found" });
    }

    // Fetch document body from R2
    const object = await env.UPLOADS_R2.get(key);
    if (!object) {
      return res.status(404).json({ error: "Document body missing" });
    }

    // Guard against excessively large objects exhausting memory
    if (object.size > MAX_BODY_BYTES) {
      return res
        .status(413)
        .json({ error: "Document exceeds maximum retrievable size" });
    }

    let body;
    try {
      body = await object.text();
    } catch (readErr) {
      return res.status(502).json({ error: "Failed to read document body" });
    }

    res.json({ metadata, body });
  } catch (err) {
    next(err);
  }
});

// For hot paths, bypass Express entirely
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Fast path — skip Express overhead for simple health checks
    if (url.pathname === "/ping") {
      return new Response("pong", { status: 200 });
    }

    // Per-request Workers bindings injected via the handler closure
    return httpServerHandler((req, res) => {
      req.cloudflare = { env, ctx };
      app(req, res);
    })(request, env, ctx);
  },
};

For endpoints that do not need middleware processing, bypassing Express entirely and returning a Workers-native Response object eliminates the framework overhead on hot paths.

For endpoints that do not need middleware processing, bypassing Express entirely and returning a Workers-native Response object eliminates the framework overhead on hot paths.

Production Deployment Checklist

  1. ☐ Set compatibility_date to the minimum date enabling node:http (verify against Cloudflare's changelog) in wrangler.toml
  2. ☐ Add nodejs_compat_v2 to compatibility_flags
  3. ☐ Audit all middleware against the compatibility matrix above
  4. ☐ Replace filesystem-dependent middleware with Workers Assets or R2
  5. ☐ Replace session storage with KV or Durable Objects
  6. ☐ Remove app.listen() (move to separate server.js) and wrap app with httpServerHandler in entry point
  7. ☐ Run wrangler dev locally and test all routes
  8. ☐ Run wrangler deploy --dry-run to catch statically bundled import failures
  9. ☐ Check bundle size against Cloudflare's limits documentation
  10. ☐ Integration test with wrangler dev --remote against preview (not production) bindings — ⚠️ --remote uses live bindings and can modify production data if not configured with preview namespaces
  11. ☐ Configure Workers Logpush or Tail Workers for observability
  12. ☐ Deploy to a staging environment with gradual traffic migration
  13. ☐ Monitor error rates and P99 latency for 48 hours before full cutover

When to Migrate and When to Wait

Express and Koa applications that primarily serve JSON APIs, use standard body parsing, and rely on external data stores are production-ready for Workers migration today. Applications that depend heavily on filesystem operations, native addons, or process-spawning patterns should wait for further runtime expansion or restructure those components first. Cloudflare maintains a Node.js compatibility tracking page that documents which APIs are supported at each compatibility date.

Run your middleware stack against the compatibility matrix above. If more than three packages show ❌, defer migration until the runtime covers those modules or you find working replacements. Otherwise, start with a non-critical service, validate in staging, and expand from there.

coursera_2026_06_footer
SitePoint TeamSitePoint Team

Sharing our passion for building incredible internet things.

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.