Deploy Background Workers

Long-running processes that just keep running. Push your worker, we keep it alive.

Perfect For

Queue Consumers

Pull jobs off SQS, Redis, RabbitMQ, or any queue and process them as they arrive

Pollers and Sync Loops

Watch an external API, sync data between systems, or reconcile state on a tight loop

Notification Dispatchers

Subscribe to events and fan out emails, push notifications, or webhooks downstream

Realtime Processors

Consume Kafka or Kinesis streams, process events, write results to your database

How It Works

  1. 1

    Write your worker

    A normal Node.js, Python, or Go process. Loop forever, await your queue, do the work.

  2. 2

    Push to Tokay

    git push tokay we detect it as a background worker.

  3. 3

    It just runs

    No URL, no schedule. Your process stays alive in the background, and we give you logs and a way to stop and start it.

  4. 4

    It stays up

    If your process crashes, we restart it. If you push a new version, we swap it in cleanly.

When to Pick a Background Worker

The four service types each fit a different shape of work. Here's the quick test:

No URL, no schedule, just keeps running

That's a Background Worker. Queue consumers, pollers, daemons.

Has a public URL people visit in a browser

Use a Web Service.

Runs on a clock

Use a Scheduled job instead. The runtime starts your process on the schedule and exits when it's done.

Receives HTTP requests from another system

Use a Function. You get a webhook URL and request history.

Just Write Your Worker

No framework to learn. Loop, do work, log. We keep the process alive.

Node.js Queue Consumer

// worker.js - Pulls jobs off SQS forever
const { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } = require('@aws-sdk/client-sqs');

const sqs = new SQSClient({ region: 'us-east-1' });
const QueueUrl = process.env.QUEUE_URL;

async function processJob(body) {
  console.log('Processing', body.id);
  // ... do the work
}

async function run() {
  console.log('Worker started');
  while (true) {
    const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
      QueueUrl,
      WaitTimeSeconds: 20,
      MaxNumberOfMessages: 10,
    }));

    for (const m of Messages) {
      await processJob(JSON.parse(m.Body));
      await sqs.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle: m.ReceiptHandle }));
    }
  }
}

run().catch((err) => {
  console.error('Worker crashed:', err);
  process.exit(1);
});

Python Poller

# poller.py - Sync an external API every 30 seconds, forever
import os, time, requests

API = os.environ['UPSTREAM_URL']
TOKEN = os.environ['UPSTREAM_TOKEN']

def sync_once():
    res = requests.get(API, headers={'Authorization': f'Bearer {TOKEN}'})
    res.raise_for_status()
    for item in res.json():
        # ... write to your database
        print('synced', item['id'])

def run():
    print('Poller started')
    while True:
        try:
            sync_once()
        except Exception as e:
            print('sync failed:', e)
        time.sleep(30)

if __name__ == '__main__':
    run()

Go Stream Processor

// main.go - Consume a Kafka topic forever
package main

import (
    "context"
    "log"
    "os"

    "github.com/segmentio/kafka-go"
)

func main() {
    r := kafka.NewReader(kafka.ReaderConfig{
        Brokers: []string{os.Getenv("KAFKA_BROKERS")},
        Topic:   "events",
        GroupID: "tokay-worker",
    })
    defer r.Close()

    log.Println("Worker started")
    for {
        msg, err := r.ReadMessage(context.Background())
        if err != nil {
            log.Fatalf("read failed: %v", err)
        }
        log.Printf("event %s: %s", msg.Key, msg.Value)
        // ... process the event
    }
}

Push it. We detect it as a background worker, keep it running, and stream you the logs.

What We Handle For You

Stays Alive

If your worker crashes, we restart it. You don't have to write a supervisor or systemd unit.

Live Logs

Stdout and stderr stream straight to the dashboard. Watch your worker do its thing in real time.

Clean Deploys

Push a new version and we swap the running process. No URL flips to coordinate, no requests to drain.

Secrets and Env

Queue URLs, API tokens, DB credentials. Manage them in the dashboard, read them as environment variables.

Run your worker without the babysitting

Push code, we keep it running. That's the whole deal.