Skip to content

[Bug] Scheduled speedtests fail with "Timeout occurred in connect" while manual tests on the same server succeed #2716

@smoochy

Description

@smoochy

Welcome!

  • I have read the documentation and my problem was not listed in the help section.
  • I have searched open and closed issues and my problem was not mentioned before.
  • I have verified I am using the latest version available. You can check the latest release here.
  • I agree to follow this project's Code of Conduct.

What did you do?

I configured Speedtest Tracker with a cron schedule (SPEEDTEST_SCHEDULE="0 */4 * * *") and a fixed server (SPEEDTEST_SERVERS=31448). I also set SPEEDTEST_EXTERNAL_IP_URL=icanhazip.com (without the https:// protocol prefix) and SPEEDTEST_INTERNET_CHECK_HOSTNAME=google.com.

Every scheduled test fails. Running a manual test for the exact same server via the Admin UI succeeds without any issues.

Before the below mentioned workaround:

Image

After the below mentioned workaround:

Image

Root Cause Analysis

After reviewing the source code, AI identified the following:

All speedtests — both scheduled and manual — run through CheckForInternetConnectionJob. This job:

  1. Attempts to ping the host configured via SPEEDTEST_INTERNET_CHECK_HOSTNAME.
  2. If the ping fails or is unavailable, it falls back to an HTTP GET request against SPEEDTEST_EXTERNAL_IP_URL.

The HTTP fallback is implemented as:

$url = config('speedtest.preflight.external_ip_url');
$response = Http::retry(3, 100)->timeout(5)->get(url: $url);

When SPEEDTEST_EXTERNAL_IP_URL is set to icanhazip.com without a protocol prefix, the HTTP client cannot resolve it as a valid URL, which causes the repeated timeout and ultimately marks the result as Failed.

Why do manual tests succeed?
A manual test triggered from the UI sets scheduled = false. In SkipSpeedtestJob the skip-IP logic is bypassed for non-scheduled tests, and in SelectSpeedtestServerJob the config-based server selection is also only applied to scheduled results. The CheckForInternetConnectionJob runs for both — however, the connectivity check appears to behave differently in practice, possibly due to race conditions, caching, or the ping succeeding intermittently for manual tests while consistently failing under the scheduler's execution context.

Workaround (confirmed working):

- SPEEDTEST_EXTERNAL_IP_URL=https://icanhazip.com/
- SPEEDTEST_INTERNET_CHECK_HOSTNAME=icanhazip.com

Note: config/speedtest.php also references an undocumented variable SPEEDTEST_CHECKINTERNET_URL that takes precedence over both of the above and sets both values at once. Since it does not appear in the documentation, the workaround above using the documented variables is recommended.


Suggested Fix

The config value speedtest.preflight.external_ip_url (mapped from SPEEDTEST_EXTERNAL_IP_URL) is used directly as a URL in an HTTP request. The application should either:

  1. Validate and normalize the configured URL at boot time, ensuring it always has a valid protocol prefix (https://), or
  2. Add a protocol prefix automatically if none is present before passing it to Http::get(), or
  3. Update the documentation to clearly state that SPEEDTEST_EXTERNAL_IP_URL must include the full URL with protocol (e.g., https://icanhazip.com), since the default value in config/speedtest.php already includes https://:
    'external_ip_url' => env('SPEEDTEST_CHECKINTERNET_URL') ?? env('SPEEDTEST_EXTERNAL_IP_URL', 'https://icanhazip.com'),
    The default is safe, but overriding it without the protocol silently breaks scheduled tests.

Expected Behavior

Scheduled speedtests should complete successfully, just like manually triggered tests for the same server.

Steps to Reproduce

  1. Deploy Speedtest Tracker via Docker Compose with the following environment variables:
    SPEEDTEST_SCHEDULE="0 */4 * * *"
    SPEEDTEST_SERVERS=<any server ID>
    SPEEDTEST_EXTERNAL_IP_URL=icanhazip.com        # ← no protocol prefix!
    SPEEDTEST_INTERNET_CHECK_HOSTNAME=google.com
    
  2. Wait for the cron schedule to trigger an automatic speedtest (or trigger it manually via the scheduler).
  3. Observe that the result is marked as Failed with the message:

    Error: [0] Timeout occurred in connect.

  4. Trigger a manual speedtest for the same server via Admin → Results → Run Speedtest.
  5. Observe that the manual test completes successfully.

Deployment Environment

Docker Compose

What is your environment & configuration?

services:
  speedtest-tracker:
    image: ghcr.io/linuxserver/speedtest-tracker:1.13.10
    container_name: speedtest-tracker
    restart: unless-stopped
    environment:
      - TZ=Europe/Berlin
      - APP_KEY=${APP_KEY}
      - APP_URL=${APP_URL}
      - DB_CONNECTION=sqlite
      - SPEEDTEST_SCHEDULE="0 */4 * * *"
      - SPEEDTEST_SERVERS=31448
      - DISPLAY_TIMEZONE=Europe/Berlin
      - PRUNE_RESULTS_OLDER_THAN=180
      - SPEEDTEST_EXTERNAL_IP_URL=icanhazip.com # ← root cause
      - SPEEDTEST_INTERNET_CHECK_HOSTNAME=google.com
      - PUID=99
      - PGID=100

Add more configuration information here.

Application Information

{
  "Laravel": {
    "version": "11.x"
  },
  "Speedtest Tracker": {
    "Version": "v1.13.10"
  }
}

What browser(s) are you seeing the problem on?

N/A — issue is fully reproducible without any browser interaction (it is a server-side scheduling problem).

Logs

.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs reviewA maintainer needs to look at this issue stillquestionFurther information is requested

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions