Skip to content

Cloudflare.Worker: changing a Config value read in Init does not redeploy the Worker (Plan: no changes) #1831

Closed

Description

@juliusmarminge

Summary

Changing a Config value that an Effect-native Worker reads in its Init body does not redeploy the Worker. alchemy deploy plans Plan: no changes / [Worker] noop, so the new value never reaches Cloudflare. The value is only uploaded when the Worker updates for another reason, such as a code change.

This affects every yield* Config.xxx(...) read in Init: feature flags, Config.Redacted secrets being rotated, and so on. Values declared statically on env (env: { X: Config.Redacted("X") }) are not affected.

Seen on [email protected]. The same code is on main at 983a8fb.

Reproduction

src/worker.ts:

import * as Cloudflare from "alchemy/Cloudflare";
import * as Config from "effect/Config";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";

export default class ReproWorker extends Cloudflare.Worker<ReproWorker>()(
  "ReproWorker",
  {
    main: import.meta.url,
    compatibility: { date: "2026-05-22", flags: ["nodejs_compat"] },
  },
  Effect.gen(function* () {
    const mode = yield* Config.String("REPRO_MODE").pipe(Config.withDefault("unset"));
    return { fetch: Effect.succeed(HttpServerResponse.text(mode)) };
  }),
) {}

alchemy.run.ts:

import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import ReproWorker from "./src/worker.ts";

export default Alchemy.Stack(
  "alchemy-config-repro",
  { providers: Cloudflare.providers(), state: Alchemy.localState() },
  Effect.gen(function* () {
    const worker = yield* ReproWorker;
    return { url: worker.url };
  }),
);

Steps:

  1. REPRO_MODE=a alchemy deploy --stage repro. The Worker is created, and curl <url> returns a.
  2. REPRO_MODE=b alchemy deploy --stage repro.

Expected: step 2 plans [ReproWorker] update, and curl <url> returns b.

Actual: step 2 plans Plan: no changes, and curl <url> still returns a.

We hit this in production. We changed a GitHub Actions environment variable read this way and reran the deploy job. The log showed the new value in the job environment and Plan: no changes, and the deployed Worker never got the value.

I haven't run these two deploys in a clean account. I confirmed the plan-side state offline instead, by evaluating the stack above with evalStack (dummy Cloudflare credentials; the plan phase makes no API calls). For both values, the stack has no bindings for ReproWorker, and the new value only appears in props.env:

REPRO_MODE=a  bindings: []  props.env.REPRO_MODE: ApplyExpr over Redacted("a")
REPRO_MODE=b  bindings: []  props.env.REPRO_MODE: ApplyExpr over Redacted("b")

The Worker diff reads the first of these and skips the second, as described below.

Cause

The value is captured correctly, but only into a part of the props that the Worker diff no longer compares:

  1. Platform.make builds the Worker's bindings in the onCreate hook, from the static props only, before Init runs (Platform.ts#L529-L530, Worker.ts#L2576).

  2. Init then runs in the plan phase, and the Config interceptor calls runtimeContext.set(key, …) for each read (Platform.ts#L586).

  3. Those values are merged into instance.Props.env only, not into the stack's bindings (Platform.ts#L661).

  4. The Worker diff then ignores props.env in both of its checks:

    Both came from fix(cloudflare/workers): converge nested yielded-resource env plans #1273. Their premise is that "every env entry is lowered into binding data by bindWorkerAsyncBindings". That holds for static env, but not for Init-captured config, which never goes through bindWorkerAsyncBindings.

This looks like a regression of the case that test/Diff.test.ts "detects a changed yielded config value in env" was written for. That test still passes because it calls havePropsChanged directly. The Worker diff no longer passes env to it.

Suggested fix

Either:

  • Lower Init-captured config into binding data. Then both the hash and the upload go through the same path as static env.
  • Or include the Init-captured part of props.env in the metadata hash. These values are plain strings or Redacted<string>, so they don't have the attribute-projection churn fix(cloudflare/workers): converge nested yielded-resource env plans #1273 was avoiding.

A regression test would deploy the fixture above, change the variable, and assert the plan is update. The existing WorkerEnv live test only checks the first deploy.

Workaround

alchemy deploy --force, or declare the value statically on env instead of reading it in Init.

Activity

  1. juliusmarminge commented on Sep 25, 2026

    @juliusmarminge
    ContributorAuthor

    Heads up on the workaround above: alchemy deploy --force currently replaces unchanged Planetscale.PostgresRole resources (new credentials, old role deleted) when the role references a PostgresDatabase. See #1832.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions