Run Background Work Inside Your Web App, Not Beside It

·5 min read·Updated
workersschedulingarchitecturevolumes

Your web app needs to do something on a schedule — expire sessions, send digests, poll an API, prune old rows. The instinct is a second application, or a cron service. Neither is the right first move here: run the background work in the same process as your web server. One container, one process, a task running alongside your request handling. That's simpler to deploy and reason about, and there's a structural reason it's often the only workable option — two applications can't share a volume, so splitting means your background job can no longer touch the same SQLite file or uploaded files your web app uses.

Same process, not a second process

To be precise about what "same app" means: one container running one process, with concurrency inside it — an async task, a background thread, a hosted service. Not two programs supervised by a shell script.

Don't reach for supervisord, a & in your entrypoint, or a process manager. The platform already restarts your process when it exits, and a supervisor inside the container means the platform sees a healthy wrapper while the thing that matters may be dead — and your app's exit code is replaced by the wrapper's, losing the most useful diagnostic you have.

The shape, in outline:

# start the loop alongside the server, in the same process
async def background():
    while True:
        try:
            await do_work()
        except Exception:
            traceback.print_exc()
        await asyncio.sleep(INTERVAL)

@app.on_event("startup")
async def start_background():
    asyncio.create_task(background())

Equivalents exist everywhere — a thread in a WSGI app, a hosted service in .NET, a supervised process in Elixir, setInterval in Node.

Why this beats a second app here

It can reach your data. A volume belongs to exactly one application, so two apps cannot read the same SQLite file or the same upload directory. If your background job needs the data your web app owns, in-process is the straightforward answer — the alternative is running a real database so both can connect to it.

No network hop. There's no private networking, so two apps talk over public endpoints with credentials. Keeping them together avoids inventing an internal API and securing it.

One deploy, one set of logs, one place to look. Two apps means two things to keep in sync and two places to check when something's wrong.

What you're accepting

Be clear-eyed about the trade, because it's real.

The background work shares your CPU budget. Your allocation is a time budget for the whole container, so a heavy background task competes with request handling. On a fractional CPU that shows up as latency spikes in your web responses while the job runs. Keep background work small, or move heavy processing off the critical path.

Never block the event loop. In an async framework, a synchronous CPU-bound call in your background task stalls every in-flight request. Use the framework's mechanism for offloading blocking work.

A restart kills whatever was in flight. Deploys and configuration changes replace the container with roughly a second's notice, so a job halfway through is simply gone. That's fine if you design for it:

  • Record what you've finished, not what you're starting.
  • Make cycles idempotent, so repeating an interrupted one is harmless.
  • Keep iterations short — then a restart costs at most one cycle.

A stalled task is invisible. Nothing watches your app, and a background loop that quietly stopped looks identical to one that's idle. Log one line per cycle including the boring ones, so silence is meaningful.

When to split into two apps

There are good reasons, and they're about behaviour, not tidiness:

  • The work must continue while you redeploy the web app — or the reverse.
  • It's genuinely CPU-heavy and would starve request handling regardless of tuning.
  • They need different resource sizes. A memory-hungry job shouldn't force you to size your web app for it.
  • Different restart cadences — a long job you don't want interrupted every time you ship a CSS fix.

If you split, sort out data sharing first, because that's the part that bites: a real database both apps connect to, or an explicit API between them. Remember that app count isn't a billing unit — you pay for allocated resources — but both apps draw from the same account limits.

Scheduling without cron

There's no platform cron. Your process runs continuously, so a loop with a sleep is the schedule.

Two details make it behave:

  • Subtract the elapsed time from your sleep, or a five-minute interval becomes seven after a two-minute job.
  • Persist the last successful run if the timing matters, so a restart doesn't skip or repeat. In-memory "next run at" is reset every deploy.

For work that must happen at a specific time of day, remember the container clock is UTC unless you set otherwise — more on that.

FAQ

Can I run a scheduled job in the same app as my website?

Yes, and it's usually the right first choice — run it as a background task in the same process rather than a second application.

Should I use supervisord to run two processes?

No. The platform supervises one process per container, and an internal supervisor hides your app's exit code and real state.

Why can't my background app share files with my web app?

A volume belongs to a single application. If both need the same data, keep them in one app or use a database both can connect to.

Will background work slow down my web requests?

It can — they share one CPU time budget. Keep the work light, and never block the event loop with synchronous work.

What happens to a running job when I deploy?

It's lost, with about a second's notice. Record completed work rather than in-progress work, and keep cycles short and repeatable.

Do I need a cron service?

No. A loop with a sleep in your long-running process is the scheduler. Subtract elapsed time and persist the last run.

Wrapping up

"Background jobs need their own service" is good advice inherited from environments where processes are cheap and networks are private. Here the opposite pressures apply: a second app can't touch your volume, can't be reached privately, and doubles what you deploy. Start with a task inside the process you already have, keep its cycles short and idempotent, log every one, and split it out only when its behaviour — not its tidiness — demands it.