A FastAPI sample showing how to run a background worker alongside an Embr-hosted app, even though Embr doesn't currently expose a worker / cron / queue-consumer app type.
The job queue is a Redis Stream (XADD / XREADGROUP) backed by Embr's embedded Valkey primitive. Each enqueued prompt is consumed by a worker thread that calls Foundry's Responses API and writes the result back into Redis under result:{job_id}.
Embr's embr.yaml only describes web apps with a single HTTP listener. There's no worker: block, no schedule:, no queue:. So this sample:
- Starts a daemon thread (
worker.start_worker_thread()) on FastAPI startup. - Lets uvicorn keep the container "alive" — Embr's health check stays green.
- Exposes the worker's status via
/healthand the dashboard so you can see whether it's actually running and what it's done.
This pattern works, but it has real downsides — see Trade-offs below. The sample exists in part to motivate adding a real worker primitive to Embr.
┌─────────────────────────────────────────────┐
│ One Embr deployment (single container) │
│ │
│ uvicorn app.main:app │
│ └─ FastAPI (HTTP) │
│ ├─ POST /api/jobs ───┐ │
│ ├─ GET /api/jobs/{id} │ │
│ ├─ GET /health │ XADD jobs │
│ └─ / │ │
│ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ Daemon thread: worker_loop() │ │
│ │ XREADGROUP > → call Foundry │ │
│ │ → SET result:{id} │ │
│ └──────────────────────────────────┘ │
│ │
│ Embedded Valkey (cache.enabled: true) │
└─────────────────────────────────────────────┘
│
▼
Foundry
(Responses API)
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
docker run -d --rm --name worker-redis -p 6380:6379 redis:7-alpine
export REDIS_URL=redis://localhost:6380/0
export FOUNDRY_BASE_URL=https://<your-foundry>.services.ai.azure.com/api/projects/<project>/openai/v1
export FOUNDRY_API_KEY=<your-key>
export FOUNDRY_MODEL_DEPLOYMENT=gpt-4.1-mini
uvicorn app.main:app --reload --port 8000Open http://localhost:8000, submit a prompt, watch it go from queued → done.
embr quickstart deploy embr-devs/embr-foundry-worker-sample
embr variables set FOUNDRY_BASE_URL "<...>" -p $PROJ -e $ENV
embr variables set FOUNDRY_API_KEY "<...>" -p $PROJ -e $ENV --secret
embr variables set FOUNDRY_MODEL_DEPLOYMENT "<deployment>" -p $PROJ -e $ENV
embr deployments trigger -c HEAD -p $PROJ -e $ENVcache.enabled: true provisions the embedded Valkey and injects CACHE_URL / REDIS_URL. No other primitive is needed.
| Concern | What's wrong |
|---|---|
| Independent scaling | Worker concurrency is coupled to web concurrency. You can't scale workers without scaling the HTTP listener. |
| Failure semantics | If the worker thread crashes but uvicorn keeps responding, Embr won't know. We surface last_error on /health to mitigate, but the platform won't act on it. |
| No retry primitive | We do XAUTOCLAIM for stuck messages but there's no built-in DLQ, exponential backoff, or visibility timeout. |
| No cron | Want to run a job every 15 minutes? You'd need to build that yourself with time.sleep in another thread. |
| Resource accounting | CPU/RAM are counted against the web pod, not the worker. |
| Long jobs | A job longer than the request timeout still works (it's not a request) but you lose any direct feedback channel — the client must poll. |
A real worker primitive (worker: block in embr.yaml) would solve all of these.
- Submit a long prompt and watch the dashboard poll until it flips to
done. - Hit
/healthwhile jobs are in flight —processedandlast_processed_atupdate live. - Submit several jobs quickly — they're all serialized through one consumer; this is a great place to ask "why doesn't Embr just give me 5 worker replicas?"
- Kill the worker by
embr deployments trigger'ing during a long job — the next worker shouldXAUTOCLAIMthe stuck message after 60s of idle time.