DEV Community

Blake Yang
Blake Yang

Posted on

The 3 AM Job That Never Ran: Taming Sleep Mode on a Free Server

At 3:00 AM, the log file should have shown a new entry, but it did not. The next night, the same silence appeared again. I had scheduled an AI-powered summary job to run on a free server, and after a few hours of inactivity, the server had quietly gone to sleep, taking my cron job with it. This is the story of how I found the problem, why it happens, and the wake-up pattern that fixed it.

The server was the free tier of MonkeyCode, an open-source project that offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The fix, however, is not specific to MonkeyCode; it applies to any free server with a sleep policy.

The Symptom: A Cron Job That Skips

The scheduled job was a simple Python script that summarized the previous day's logs and sent the result to a webhook. It was configured to run at 3:00 AM via cron, and it worked for the first two days. On the third day, the webhook received nothing, and the cron log showed no entries for the entire night. The server process was still running, and the system clock was correct, which ruled out the most obvious causes.

The First Suspect: Timezone and Syntax

The first step was to verify the cron configuration itself. The crontab entry used the correct server timezone, and the syntax matched the standard five-field format. A manual test of the command worked without errors. This eliminated timezone confusion and syntax mistakes, leaving the cron daemon or the server environment as the remaining suspects.

The Real Culprit: Sleep Mode

The system logs revealed the truth: the server had entered a sleep state after roughly an hour of inactivity. Many free tiers spin down idle processes to conserve resources, and the MonkeyCode free server appears to follow the same pattern. When the server sleeps, the cron daemon is suspended, so scheduled jobs simply do not fire until the server wakes up again.

The Wake-Up Pattern

The solution is to keep the server awake by sending periodic HTTP requests from an external service. A cloud-based cron service such as cron-job.org can hit a lightweight endpoint on your server every few minutes. Each request wakes the server if it is sleeping, and the endpoint can also check whether the scheduled job is due and trigger it immediately.

A Minimal Flask Wake-Up Endpoint

The following Flask application exposes a /wake endpoint and runs the scheduled job in a background thread. The external cron service calls this endpoint every five minutes, which prevents the server from sleeping and also provides a reliable trigger for the 3 AM job.

from flask import Flask
import time
import threading

app = Flask(__name__)

job_due = False
JOB_HOUR = 3

def run_scheduled_job():
    # Replace with your AI summarization logic
    print("Running the scheduled job at", time.strftime("%Y-%m-%d %H:%M:%S"))

@app.route("/wake")
def wake():
    global job_due
    job_due = True
    return "OK"

def worker():
    global job_due
    last_run_day = None
    while True:
        if job_due:
            now = time.localtime()
            if now.tm_hour == JOB_HOUR and now.tm_min < 10 and last_run_day != now.tm_yday:
                run_scheduled_job()
                last_run_day = now.tm_yday
            job_due = False
        time.sleep(1)

if __name__ == "__main__":
    threading.Thread(target=worker, daemon=True).start()
    app.run(host="0.0.0.0", port=3000)
Enter fullscreen mode Exit fullscreen mode

The external cron service should be configured to send a GET request to https://your-server.example.com/wake every five minutes. The endpoint marks the job as due, and the background thread runs it exactly once per day by tracking the last run day.

The Idempotency Trap

A naive implementation can run the job twice if the external cron request arrives while the job is still executing. The fix is to add a lock that prevents concurrent runs. A simple file lock works for a single-process server, but a database-based lock is safer if you scale to multiple workers.

import os

LOCK_FILE = "/tmp/scheduled_job.lock"

def run_with_lock():
    if os.path.exists(LOCK_FILE):
        return
    open(LOCK_FILE, "w").close()
    try:
        run_scheduled_job()
    finally:
        os.remove(LOCK_FILE)
Enter fullscreen mode Exit fullscreen mode

This lock is not perfect, because a crash can leave a stale lock file. A more robust approach uses an atomic filesystem operation or a database row with a unique constraint. For a free-tier experiment, the simple lock is usually enough.

Testing the Wake-Up Pattern

To verify the pattern, you can simulate sleep by stopping the Flask app, then start it again and send a wake request. The logs should show the job running if the request arrives within the scheduled window. A more thorough test uses a fake clock to confirm that the job runs exactly once even when the external cron service fires multiple times.

Limitations and Who Should Skip This

The wake-up pattern depends on an external cron service, which adds a third-party dependency and a small amount of latency. Free servers may also limit the number of requests per minute, so a five-minute interval is usually safe, but a shorter interval could trigger rate limits. If the server completely stops rather than sleeps, the wake request may not be enough to start it, and a manual restart might be required. This approach is best for jobs that can tolerate a few minutes of delay and do not need second-level precision.

The Bottom Line

Free tiers are a great way to experiment with AI workloads, but they come with infrastructure quirks that are rarely documented. Sleep mode is one of those quirks, and the wake-up pattern is a simple, server-agnostic workaround. MonkeyCode's free tier includes a 10-million-token allowance and a free server option, which makes it a convenient place to test this pattern. If you are running scheduled AI jobs on a budget, the wake-up endpoint is a small piece of code that can save you a lot of missed runs.

Top comments (0)