Skip to content

Regression of #199 in 2.x: scheduler effectively runs every ~10 minutes, schedule:run | awk | tee pipeline blocks on FreeScout’s background queue:work daemon (EXPERIMENTAL_CRON_BACKGROUND is gone) #265

Description

@Peps714

Environment

  • Image: nfrastack/freescout:latest (image 2.1.3, FreeScout 1.8.226)
  • SCHEDULER_TYPE=CRON (default); the ALT mode is affected as well (see below)

Summary
FreeScout’s artisan schedule:run intentionally spawns a long-running queue:work daemon (its own worker-supervision logic; the worker typically lives ~10 minutes before being cycled). The cron entry generated by the image wraps schedule:run in a shell pipeline:

* * * * * { echo "...Scheduling start"; sudo -u nginx TZ=... php /www/html/artisan schedule:run | awk '{ ... }'; echo "...Scheduling Finished"; } | tee -a /logs/laravel/scheduler.log

The spawned queue:work daemon inherits stdout connected to the awk/tee pipe, so the pipeline does not complete until the worker exits (~10 minutes). Since job runs are serialized, the effective scheduler cadence becomes one schedule:run per ~10 minutes instead of one per minute.

The most painful consequence: freescout:fetch-emails is scheduled every minute, but incoming mail is actually fetched only every ~10 minutes.

Evidence
scheduler.log from a stock container (note the gap between “start” and “Finished”, and no cycles in between):

2026-07-13.06:00:03 Scheduling start
2026-07-13 06:00:04 Running scheduled command: ... freescout:fetch-emails ...
2026-07-13 06:00:06 Running scheduled command: ... queue:work --queue='emails,default,...' --sleep=5 --tries=1 --timeout=1800 ...
2026-07-13.06:09:33 Scheduling Finished
2026-07-13.06:10:00 Scheduling start

Laravel log also shows workers being SIGTERM’ed by FreeScout’s duplicate-worker cleanup when cycles overlap after cache clears:

production.ERROR: The process has been signaled with signal "15".
  ... Illuminate\Console\Scheduling\Event->runCommandInForeground(...)

ALT scheduler is affected too
/container/run/available/41-freescout-scheduler/run has the same construction inside a sequential loop:

while true; do
    echo "$(_ts) Scheduling Starting"
    sudo -u "${NGINX_USER}" TZ="${TIMEZONE}" php "${NGINX_WEBROOT%/}"/artisan schedule:run | \
        TZ="${TIMEZONE}" awk '{ print strftime("%Y-%m-%d.%H:%M:%S"), $0; fflush() }'
    echo "$(_ts) Scheduling Finished"
    _sleep_until_next_minute
done

Suggested fixes

  1. Don’t let the wrapper wait on schedule:run’s descendants: redirect output to the log file directly (losing the awk timestamps or using a non-blocking stamper), e.g. php artisan schedule:run >> "$log" 2>&1, so the inherited-stdout pipe cannot hold the cycle open; or
  2. Run schedule:run --no-interaction: FreeScout skips launching its internal queue:work daemon when -n is passed — combined with a dedicated worker service this gives clean 1-minute cycles. Note: the existing 30-laravel-worker service runs plain php artisan queue:work, which listens only to the default queue; FreeScout dispatches to emails,default,{instance-identifier} (see config('app.queue_work_params') and Helper::getWorkerIdentifier()), so the service must pass those queue parameters, otherwise outgoing-mail jobs are never processed.

History — this is a regression of #199
The same symptom was reported for the tiredofit-era image in #199 (“Freescout cronjob delayed for 10 min after container start”; users saw mail fetch delays from 10 minutes up to 8–10 hours). The mechanics were identified there: schedule:run keeps FreeScout’s queue:work daemon in its process group, and busybox crond skips subsequent runs while the previous one is still “running”. A workaround env EXPERIMENTAL_CRON_BACKGROUND=TRUE was added in 1.17.114.

That variable does not exist in the current 2.x codebase (verified inside image 2.1.3: no CRON_BACKGROUND/EXPERIMENTAL handling in /container/defaults or /container/functions), and the new | awk | tee log pipeline re-introduces the blocking — now on every cycle, not just container start. Please consider reinstating a supported detach/background option for the scheduler.

Workaround we currently use
A host-level systemd timer runs docker exec --user nginx php artisan schedule:run --no-interaction every minute; the container’s own cron remains in place solely to supervise queue:work. Laravel’s withoutOverlapping mutexes (shared application cache) prevent duplicate task runs between the two schedulers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions