Un job in background è un compito che gira fuori dal ciclo della richiesta: accodato dall’app, eseguito dai worker, ritentato in caso di errore. La linea di demarcazione è il tempo: una risposta deve sembrare istantanea (qualche centinaio di millisecondi) e deve battere il timeout del gateway (tipicamente circa 30 secondi), mentre il lavoro reale – e-mail via SMTP, elaborazione di immagini, generazione di report, API di terze parti – richiede da secondi a minuti e merita dei ritentativi. Tutto ciò che sta dalla parte sbagliata di quella linea viene accodato, confermato e svolto altrove; su larga scala è infrastruttura centrale, non idraulica – una grande azienda di messaggistica elabora oltre un miliardo di job al giorno esattamente con questo macchinario.
Punti chiave
| Domanda | Risposta |
|---|---|
| L’architettura | Il produttore accoda → la coda persiste → il worker esegue e conferma |
| La linea di demarcazione | Budget di risposta circa 300 ms; tutto ciò che è lento o ritentabile diventa un job |
| Le tre pianificazioni | Immediata · differita (“tra 24 h”) · ricorrente (cron) |
| Il contratto di affidabilità | At-least-once + job idempotenti + backoff con jitter + dead letter |
| L’errore silenzioso | Niente dà errore quando un job pianificato non gira – va sorvegliata l’assenza |
L’anti-pattern che insegna tutto
// ✗ La registrazione che si fa ostaggio di un server di posta
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user); // SMTP lento? Chi si registra fissa uno spinner.
res.json(user); // SMTP giù? La registrazione dà 500 – ma
}); // l'account ESISTE. Il peggio di tutti i mondi.
// ✔ Accodare e rispondere
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await jobs.enqueue('welcomeEmail', { userId: user.id }); // millisecondi
res.json(user); // l'e-mail parte, ritenta e riesce
}); // secondo i propri tempi – in modo invisibile
Lo stesso principio visto dai due lati di un backend reale – un job che lavora sui dati, e un client che accoda invece di aspettare:
// JavaScript — Cloud Code (cloud/main.js)
// A background job: heavy work outside the request cycle
Parse.Cloud.job('sendWeeklyDigest', async (request) => {
const query = new Parse.Query(Parse.User).equalTo('digestOptIn', true);
await query.eachBatch(async (users) => {
for (const user of users) await sendDigest(user); // per-record, resumable
}, { useMasterKey: true });
request.message('Digest run complete'); // status visible in the dashboard
});
// Run on demand or on a schedule from the dashboard — no queue to operate // Flutter / Dart — Back4app Flutter SDK
// The client's rule: never wait on heavy work — enqueue and move on
final export = ParseObject('ExportRequest')
..set('user', currentUser)
..set('status', 'queued'); // an afterSave trigger starts the work
await export.save(); // returns in milliseconds
// Watch the job's progress like any other data:
final sub = await LiveQuery().client.subscribe(
QueryBuilder<ParseObject>(ParseObject('ExportRequest'))
..whereEqualTo('objectId', export.objectId));
sub.on(LiveQueryEvent.update, (job) {
if (job.get<String>('status') == 'done') openReport(job.get('fileUrl'));
}); // iOS / Swift — Back4app Swift SDK
// The client's rule: never wait on heavy work — enqueue and move on
var export = ExportRequest()
export.status = "queued" // an afterSave trigger starts the work
let saved = try await export.save() // returns in milliseconds
// Watch the job's progress like any other data:
let sub = try await ExportRequest.query("objectId" == saved.id).subscribe()
sub.handleEvent { _, event in
if case .updated(let job) = event, job.status == "done" {
openReport(job.fileUrl)
}
} // Android / Kotlin — Back4app Android SDK
// The client's rule: never wait on heavy work — enqueue and move on
val export = ParseObject("ExportRequest")
export.put("user", ParseUser.getCurrentUser())
export.put("status", "queued") // an afterSave trigger starts the work
export.save() // returns in milliseconds
// Watch the job's progress like any other data:
val q = ParseQuery.getQuery<ParseObject>("ExportRequest")
q.whereEqualTo("objectId", export.objectId)
val sub = ParseLiveQueryClient.Factory.getClient().subscribe(q)
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, job ->
if (job.getString("status") == "done") openReport(job.getString("fileUrl"))
} Produttore, coda, worker
Tre ruoli, un contratto. Il produttore – di solito un handler di richiesta – crea un job: un nome di tipo e un piccolo payload. La coda lo persiste; la durabilità è tutto il punto, perché un lavoro che esiste solo nella memoria di un processo in agonia muore con lui. I worker prelevano, eseguono e confermano (ack); un job sparisce solo dopo la conferma, ed è così che il job di un worker andato in crash sopravvive per girare di nuovo. Il vocabolario che viaggia insieme: enqueue/dequeue, ack, visibility timeout, e la distinzione che vale la pena difendere – una coda di messaggi trasporta dati tra servizi (e può diffonderli verso molti sottoscrittori); una coda di job esegue lavoro, esattamente una volta per job e per worker, con ritentativi e stato integrati.
I tre modi di pianificazione – e un’introduzione a cron
Ogni sistema di job implementa gli stessi tre modi temporali: immediato (accodato ora, eseguito appena un worker si libera), differito (accodato ora, eleggibile in un momento futuro – promemoria, scadenze dei periodi di prova e il meccanismo stesso dei ritentativi) e ricorrente (uno scheduler riaccoda secondo un calendario). Le pianificazioni ricorrenti si scrivono quasi sempre con i cinque campi di cron:
┌ minuto (0-59) ┌ ora (0-23) ┌ giorno del mese ┌ mese ┌ giorno della settimana
0 2 * * * → ogni giorno alle 02:00
*/15 * * * * → ogni 15 minuti
0 9 * * 1 → ogni lunedì alle 09:00
Due mine dello scheduler: i passaggi all'ora legale (le 02:30 svaniscono o
avvengono due volte – conviene pianificare in UTC) e la sovrapposizione (un
job che supera il proprio intervallo richiede un lock, altrimenti due copie
elaborano gli stessi dati).
Code di job vs. code di messaggi vs. scheduler
| Coda di job | Coda di messaggi | Scheduler | |
|---|---|---|---|
| Unità | Un job da eseguire | Un messaggio da consegnare | Un orario di scatto |
| Consumatori | Esattamente un worker | Uno o molti sottoscrittori | I job che accoda |
| Integrati | Ritentativi, stato, priorità, ritardo | Durabilità, routing, fan-out | Calendari, ricorrenza |
| Nomi open source | Sidekiq, Celery, BullMQ | RabbitMQ, Kafka, Redis | cron, Quartz |
| Si combina come | Esegue ciò che gli eventi richiedono | Trasporta tra sistemi | Alimenta la coda al momento giusto |
La combinazione è la risposta a quasi tutti i dibattiti su “quale dei tre?”: gli scheduler decidono il quando, le code tengono il che cosa, i worker fanno il lavoro – e un batch notturno è cron che accoda job che i worker eseguono con tutta la semantica dei ritentativi.
Il contratto di affidabilità
I pezzi si insegnano di solito separatamente; sono un unico contratto. La coda promette at-least-once – un worker che va in crash dopo il lavoro ma prima della conferma significa che il job gira di nuovo; è inevitabile, non sciatto. In cambio si promette idempotenza: deduplicazione su un ID di job, protezione con vincoli di unicità, scritture in upsert, così la seconda esecuzione è un no-op invece di una seconda fattura. I ritentativi con backoff esponenziale e jitter colmano i guasti transitori – 1 s, 2 s, 4 s, con una componente casuale perché mille job che falliscono insieme non ritentino insieme, una cortesia che i sistemi di terze parti con rate limiting impongono comunque se non viene concessa. La dead letter queue raccoglie il resto: dopo il tetto dei tentativi, i job si parcheggiano dove le persone possono ispezionarli, correggerli e rigiocarli – e i job malformati in modo permanente dovrebbero saltare del tutto il teatro dei ritentativi e andare direttamente lì.
Progettare bene i job
Le regole su cui i framework di coda concordano, messe insieme: passare ID, non oggetti – un payload di job che contiene userId: "u-8fk2" ricarica lo stato aggiornato al momento dell’esecuzione, mentre un oggetto utente serializzato è già vecchio nell’istante in cui viene accodato; tenere i payload piccoli e semplici come JSON, mai segreti; un job per record – un job per utente che fallisce ritenta un solo utente, un mega-job ritenta tutti; rendere riprendibile il lavoro lungo – spezzarlo in blocchi, salvare il progresso a checkpoint e uscire con grazia sui segnali di arresto a metà blocco; e dare per scontata la concorrenza – un giorno due worker elaboreranno job adiacenti che toccano la stessa riga, e quella è una questione di lock a cui si risponde in fase di progetto oppure durante un incidente.
Sorvegliare la coda
L’errore in background è silenzioso: nessuno vede una pagina di errore quando il job del digest muore. I quattro indicatori che sostituiscono la pagina di errore: la profondità della coda (un arretrato che si accumula significa che i worker stanno perdendo), l’età del job misurata dall’accodamento al completamento – un job elaborato in 200 ms dopo 40 minuti di attesa è un job da 40 minuti per chi lo aspetta; i tassi di errore e di dead letter con avvisi sulla dead letter queue, perché i job parcheggiati e dimenticati sono dati che in silenzio non vengono elaborati; e le esecuzioni mancate per il lavoro pianificato – il caso più insidioso, perché una pianificazione che non è mai scattata non ha prodotto nessun errore, nessuna riga di log e nessun job. L’allarme va messo sull’assenza.
Casi d’uso tipici
- E-mail e notifiche – il rinvio canonico: accodare alla registrazione, consegnare con i ritentativi.
- Elaborazione multimediale – ridimensionamenti, transcodifiche, anteprime: minuti di CPU che nessuna richiesta dovrebbe attendere.
- Report ed esportazioni – generare in background, notificare quando il file è pronto.
- Sincronizzazione con terze parti – sincronizzazioni CRM e consegne di webhook, con backoff contro remoti instabili.
- Pulizia e manutenzione – passate di TTL, potatura degli orfani, costruzione dei digest sul cron notturno.
Quale pattern serve? Matrice decisionale
| Il lavoro assomiglia a | Scegliere |
|---|---|
| Attivato da azioni delle persone, volume variabile | Coda di job + worker |
| Calendario fisso, limitato, prevedibile | Job pianificato (cron) |
| Batch notturno su molti record | Cron accoda; i worker eseguono record per record |
| Servizi che si dicono delle cose | Coda di messaggi / pub-sub |
| Lavoro lento che oggi sta dentro un handler di richiesta | Il refactoring qui sopra – accodare e rispondere |
| Deve sopravvivere ai crash e ritentare in sicurezza | Qualsiasi di questi – più il contratto di affidabilità |
Limiti e trade-off
- Alla fine, non all’istante. Il lavoro accodato avviene dopo – le interfacce hanno bisogno di stati di attesa, e quel “dopo” ha bisogno di un limite scelto con intenzione.
- Lo stato esce dal canale principale. I risultati arrivano via campi di stato, callback o notifiche – la semplicità di richiesta-risposta è spesa, e l’idraulica va messa in bilancio.
- L’infrastruttura è reale. Broker, worker e scheduler sono servizi con le proprie modalità di guasto – oppure un problema della piattaforma, che è l’argomento a favore dei job gestiti.
- I duplicati sono garantiti, prima o poi. At-least-once è il contratto; ogni job non idempotente è un incidente a orologeria.
- Le code assorbono il sovraccarico con grazia – con troppa grazia. Un arretrato che cresce sembra tranquillo fino al momento in cui l’età dei job esplode; gli allarmi su profondità ed età sono il meccanismo di onestà.
I job in background su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. I job qui sono Cloud Code con una pianificazione: si definisce Parse.Cloud.job – le schede di codice mostrano un job di digest che procede a lotti sugli utenti con eachBatch, record per record e riprendibile – e si esegue su richiesta oppure con una ricorrenza in stile cron dalla dashboard, con stato e log nel pannello Jobs; nessun broker da provisionare, nessuna flotta di worker da scalare, nessun servizio di coda da tenere in vita. La metà guidata dagli eventi si compone dalle stesse primitive: un handler di richiesta scrive una riga con status: "queued", un trigger afterSave avvia il lavoro, e i client seguono il progresso con le Live Queries invece di fare polling – il pattern produttore/worker espresso come dati, con il contratto di affidabilità (ID nei payload, handler idempotenti, uno stato su cui mettere un allarme) come disciplina di progetto invece che come infrastruttura.
Domande frequenti
Che cos'è un job in background?
Un compito che l'applicazione esegue fuori dal ciclo richiesta-risposta: il server accoda il lavoro, risponde subito a chi ha fatto la richiesta, e un worker separato lo esegue in modo asincrono – con ritentativi in caso di errore. Tutto ciò che è lento, ritentabile o inutile alla risposta appartiene a quel posto.
Perché non fare semplicemente il lavoro nell'handler della richiesta?
Perché il lavoro lento blocca la risposta, immobilizza la capacità del server e va a sbattere contro i timeout del gateway – e se il lavoro va in errore a metà richiesta, l'utente riceve un fallimento anche se una parte è già avvenuta. L'esempio canonico: inviare l'e-mail di benvenuto durante la registrazione, dove un server di posta lento trasforma la creazione dell'account in una rotella che gira.
Come funziona una coda di job?
È una lista durevole tra produttori e worker: l'applicazione inserisce un job – un tipo più un piccolo payload – la coda lo persiste, e i worker lo prelevano, lo eseguono e lo confermano. I job non confermati tornano nella coda, ed è da lì che viene l'affidabilità.
Qual è la differenza tra una coda di job e una coda di messaggi?
La finalità. Una coda di messaggi sposta dati tra servizi: la consegna è l'obiettivo, e un messaggio può diffondersi verso molti consumatori. Una coda di job esegue lavoro: aggiunge sopra ritentativi, pianificazione, priorità e stato, e ogni job va a esattamente un worker.
Qual è la differenza tra cron job e job in background?
Il trigger. Cron è guidato dal tempo – "ogni notte alle 2"; i job in background sono guidati dagli eventi – "quando qualcuno carica un file". I due pattern si combinano: un progetto diffuso affida a cron l'accodamento del batch notturno, mentre i worker lo eseguono con tutta la semantica dei ritentativi.
Come funzionano i ritentativi dei job?
I job falliti girano di nuovo automaticamente con un backoff esponenziale – un secondo, poi due, quattro, otto – più un jitter casuale perché mille errori non ritentino tutti nello stesso istante, entro il limite di un numero massimo di tentativi. Il backoff colma i guasti transitori; il tetto impedisce a quelli permanenti di ritentare all'infinito.
Perché i job in background devono essere idempotenti?
Perché le code promettono un'esecuzione at-least-once: un worker può andare in crash dopo aver fatto il lavoro ma prima di confermarlo, e il job gira di nuovo. Girare due volte non deve addebitare due volte – chiavi di idempotenza, vincoli di unicità e upsert trasformano i duplicati in no-op.
Che cos'è una dead letter queue?
Il posto dove finiscono i job che hanno esaurito i ritentativi: conservati per ispezione, avvisi e rigioco manuale, invece di ritentare all'infinito o sparire in silenzio. I job malformati che non potranno mai riuscire dovrebbero saltare i tentativi e andare direttamente lì.