Skip to content
Console →
Website →
Asking an AI? Paste this URL https://keelson.dev/llms.txt

How Apps Run

Most problems that survive a green deployment come from one assumption: that the application is a process that stays alive. It is not. This page states when your server-side code runs, and what that rules out.

If you only remember one sentence: server-side code is guaranteed to run while a request is in flight, and during a scheduled job you declared. There is no third time.


An application starts when a request arrives. After a period without traffic it stops, and the next request starts it again — so the first response after an idle period takes longer than usual.

Server-side code therefore runs in exactly two windows:

  1. While a request is being handled — a page view, an API call, a form submission.
  2. During a scheduled job you declared in crons — Keelson starts a separate instance, runs the command, and the instance exits.

Nothing runs in between. Code written for a resident server — a scheduler that fires at 09:00, a loop that polls an external service, work handed off after the response has been sent — does not run at all.

Front-end code in the browser is not affected by any of this. A timer or an auto-refresh in the page keeps working while the page is open.


None of the following produces an error. The deployment succeeds, the logs are silent, and the screen looks correct. That is what makes this list worth reading before you ship.

What the code doesWhat actually happensWhat to do instead
An in-process scheduler sends a report every morning (node-cron, APScheduler, setInterval plus a clock check, threading.Timer)Nothing happens at the scheduled timeDeclare it under crons
Work is handed off after the response is returned (FastAPI BackgroundTasks, a fire-and-forget promise)The screen reports success and the email never arrivesFinish it before responding, or record it and drain it from a crons job
The app returns “accepted” and the browser polls for a result computed in the backgroundThe status stays “processing” foreverRecord the work in the database and drain it from a crons job — and tell the user the wait can be up to one interval
A resident loop watches or synchronizes an external serviceIt stops when the app stopsDeclare a crons entry that does one pass and exits
State is kept in a module-level variable, an in-memory session, or a local fileIt disappears when the app stopsPut it in Managed SQLite

There is no queue service and no way for the app to trigger a run of itself. Keelson does not provide an API for submitting arbitrary asynchronous tasks, and event-driven background tasks are not currently supported. The workers: declaration that used to exist has been retired: a keelson.yaml that contains workers: — even an empty one — is rejected at deployment with workers_not_supported.


Do it inside the request. Anything whose success the caller needs to know about — validating, saving, a short external call — belongs before the response, where its failure can be reported.

Put a time-of-day or periodic task in crons. Move the job body into its own entrypoint that runs to completion and exits, then delete the in-process scheduler.

crons:
- name: daily-report
schedule: "0 9 * * *"
command: "node report.js"
timeout: 300

See Scheduled Jobs for the full configuration, plan limits, and worked examples.

Write down durable work and drain it from a crons job. For a side effect the user should not wait for — an email, a webhook, a sync — the request handler inserts a row into Managed SQLite and a scheduled job claims those rows, does the work, and marks them done. Three things are not optional here:

  • The drain must be idempotent. A run can be stopped at its timeout after the side effect but before the row is marked done, and the next run will see that row again.
  • Count attempts and give up. There is no automatic retry and no dead-letter queue. A row that fails forever is drained forever.
  • Tell the user about the delay before you build it. The row waits until the next run, and the shortest interval any plan allows is five minutes. “We will email you right away” is not a promise this shape can keep.

Stream output the user is waiting on. Server-sent events or chunked responses keep the work inside the request window, so the user sees progress instead of a spinner. The window ends when the client disconnects, so streaming is not a substitute for durable work.


LimitValue
Time to start responding to a request (first response header)120 seconds; exceeding it returns 504
Total lifetime of one request, streaming included300 seconds
Gap between chunks of a streamed response120 seconds
One scheduled-job runthe job’s timeout, 1–600 seconds, default 300

A 504 does not mean the work was rolled back — the handler may finish after the deadline. Keelson never retries a POST on your behalf, so make writes that a client might retry idempotent.

Scheduled jobs have their own rules that follow from running on a schedule rather than on demand: a run may start late, a run that is still going when the next occurrence arrives causes that occurrence to be skipped rather than queued, a failed run is not retried, and a run still executing at its timeout is stopped. Recovery is the next run.

Schedules are evaluated in the workspace time zone, which is set when the workspace is created and cannot be changed afterwards. Confirm the effective value before assuming your own local time.

The number of jobs per app, the shortest interval, the maximum timeout, and the monthly execution allowance all depend on the plan — see Plans and Limits.


Paste this into the tool that built the app:

In this Keelson app, server-side code is only guaranteed to run while a request is being handled and during a declared crons run. Find every in-process scheduler (node-cron, APScheduler, setInterval, threading.Timer) and everything deferred until after the response is sent, and either finish that work inside the request or move it to a crons entry in keelson.yaml. workers: no longer exists and there is no task-submission API. Leave front-end timers alone.


  • A daily or weekly task never runs, and nothing is logged.
  • A confirmation email or notification never arrives, although the screen said the operation succeeded.
  • A “processing” state never completes.
  • Data entered a moment ago is gone after a period of inactivity.