Perché un ciclo che chiama l'API prodotto per prodotto va in blocco al terzo fornitore, come si costruisce un ponte che regge il limite di Shopify, e il numero che conta davvero: quanti ordini si vendono su uno stock che non esiste più.
di Nadia Maar ·
Il dropshipping con più fornitori ha un problema che non è commerciale: è idraulico. Tre listini che cambiano prezzo e giacenza più volte al giorno devono confluire in un negozio Shopify solo, e la prima cosa che chiunque scrive per farlo — un ciclo che chiama l'API una volta per prodotto — funziona benissimo su 200 articoli e smette di funzionare esattamente quando il catalogo cresce abbastanza da valere la pena di automatizzarlo.
Non si rompe con un errore chiaro. Si rompe a metà: duemila prodotti aggiornati, ottomila no, e il negozio per qualche ora mostra un misto di prezzi vecchi e nuovi finché qualcuno non se ne accorge. Il danno peggiore non è il ritardo, è l'overselling — vendere un articolo che il fornitore ha già esaurito, perché la sincronizzazione che avrebbe dovuto dirvelo era in coda dietro altre novemila chiamate.
Questo pezzo è l'architettura che regge: come si trasforma un catalogo di fornitori in una fonte unica di verità, come si rispetta il limite di velocità di Shopify senza rincorrerlo a tentativi, e il codice vero di un client che si ferma da solo prima di essere bloccato.
Il codice che tutti scrivono per primo è un ciclo: per ogni prodotto del fornitore, chiama l'API di Shopify e aggiorna prezzo e giacenza. È corretto, è leggibile, e regge fino al momento in cui i prodotti diventano abbastanza da superare il limite di chiamate che Shopify impone — un limite pensato per proteggere la piattaforma, non il vostro sincronismo.
Se il tempo totale della sincronizzazione cresce più che linearmente col numero di prodotti — il doppio dei prodotti impiega più del doppio del tempo — il secchio si sta già svuotando e il vostro codice sta aspettando in ritardo crescente ad ogni chiamata. È il momento di fermarsi, non di aggiungere un timeout più lungo.
La soluzione non è chiamare l'API più in fretta: è chiamarla meno. Il sistema che regge divide il lavoro in tre fasi separate, ciascuna con un compito solo, e fra la seconda e la terza c'è il passaggio che risolve l'overselling: si sincronizza solo quello che è CAMBIATO, non l'intero catalogo ad ogni giro.
Un formato solo per tre fornitori — Ogni feed fornitore — CSV, XML, o una chiamata API propria — entra qui e ne esce nello stesso schema: codice canonico, prezzo, giacenza, timestamp. La tabella di corrispondenza fra il codice del fornitore e il codice canonico vive qui, scritta e versionata, non dedotta al volo.
Riceve: Tre feed fornitore, tre formati. Produce: Un elenco solo, un formato solo
Solo le righe che sono cambiate — L'elenco normalizzato si confronta con l'ultimo stato noto, riga per riga. Un prodotto il cui prezzo e la cui giacenza non sono cambiati non genera nessuna chiamata: è la differenza fra sincronizzare 12.000 prodotti e sincronizzarne le 340 righe che sono davvero cambiate da un'ora fa.
Riceve: Elenco normalizzato + ultimo stato. Produce: Solo le differenze, con priorità
Una coda che rispetta il secchio — Le differenze entrano in una coda che chiama Shopify rispettando il costo dichiarato da Shopify stessa in ogni risposta — non un numero fisso di chiamate al secondo, ma la lettura in tempo reale di quanto secchio resta pieno. I cali di giacenza a zero passano per primi: sono quelli che prevengono l'overselling.
Riceve: Le differenze, in ordine di priorità. Produce: Il negozio allineato, senza blocchi
La fase che cambia tutto è la seconda. Un catalogo di 30.000 prodotti aggiornato ogni ora quasi mai ha più di qualche centinaio di righe realmente cambiate — il resto sono prezzi identici a un'ora fa. Confrontare prima di scrivere trasforma una sincronizzazione da dodicimila chiamate a poche centinaia, ed è la differenza fra restare dentro il secchio e svuotarlo ogni volta.
L'API GraphQL di Shopify dichiara il costo di ogni chiamata e quanto secchio resta nella risposta stessa — non serve indovinare, serve leggerlo. Questo è un client minimo che aspetta il tempo giusto invece di scoprire il limite a suon di errori 429.
async function chiamaConRispetto<T>(query: string, variabili: object): Promise<T> {
const risposta = await fetch(SHOPIFY_GRAPHQL_URL, {
method: "POST",
headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables: variabili }),
})
const corpo = await risposta.json()
const costo = corpo.extensions?.cost?.throttleStatus
if (costo && costo.currentlyAvailable < costo.maximumAvailable * 0.2) {
// Il secchio è sotto il 20%: si aspetta il tempo di ricarica invece
// di continuare a chiamare e scoprire il blocco alla chiamata dopo.
const mancante = costo.maximumAvailable * 0.5 - costo.currentlyAvailable
const attesaMs = (mancante / costo.restoreRate) * 1000
await new Promise(r => setTimeout(r, attesaMs))
}
if (corpo.errors?.some((e: { extensions?: { code?: string } }) =>
e.extensions?.code === "THROTTLED")) {
// Bloccati comunque: backoff esponenziale con jitter, non un ritentativo
// immediato che rientrerebbe nello stesso secchio ancora vuoto.
const attesa = 1000 * (1 + Math.random())
await new Promise(r => setTimeout(r, attesa))
return chiamaConRispetto(query, variabili)
}
return corpo.data as T
}Le due condizioni non sono ridondanti: la prima previene il blocco leggendo il secchio PRIMA di chiamare di nuovo; la seconda gestisce il caso in cui un'altra parte del sistema — un'app installata, un webhook — ha consumato secchio nel frattempo, cosa che nessun conteggio locale può prevedere.
Notate che cosa NON fa questo codice: non calcola «quante chiamate al secondo posso fare» come numero fisso. Shopify cambia il costo per punto in base al piano del negozio e al tipo di operazione, e un numero fisso scritto oggi è sbagliato il giorno in cui cambiate piano. Leggere il secchio dalla risposta è l'unica versione che non scade.
Un cliente con tre fornitori e circa 30.000 SKU condivisi è passato da una sincronizzazione notturna a batch — con la finestra di rischio di dodici ore in cui il sito poteva vendere stock già esaurito — a un ciclo di differenze ogni quindici minuti sulla stessa infrastruttura, senza cambiare piano Shopify.
Se avete un fornitore solo e meno di duemila SKU, un ciclo semplice con un ritardo fisso fra le chiamate è sufficiente e costruire tre fasi separate sarebbe complessità pagata per un problema che non avete ancora. Il segnale per passare all'architettura a tre fasi è lo stesso della prima sezione: il tempo di sincronizzazione che cresce più che linearmente, o il primo overselling che genera un rimborso.
Se sincronizzate già con Shopify e la finestra di rischio vi sembra troppo lunga, portate i numeri: quanti fornitori, quanti SKU, ogni quanto aggiornate oggi. In mezz'ora capiamo se il problema è il volume, il formato dei feed, o semplicemente il ciclo che chiama l'API senza leggere il secchio.
Se il catalogo è ancora sotto le duemila righe, ve lo dico: non vi serve questa architettura, vi basta un ritardo fisso ben calibrato. Costruirla ora sarebbe complessità pagata in anticipo per un problema che forse non arriverà mai.
Dividendo il lavoro in tre fasi separate: normalizzare i feed dei fornitori in un formato solo, confrontare con l'ultimo stato noto per isolare SOLO le righe cambiate, e scrivere su Shopify rispettando il costo che l'API stessa dichiara in ogni risposta. Il passaggio che conta è il secondo: un catalogo di 30.000 prodotti aggiornato ogni ora raramente ha più di qualche centinaio di righe davvero cambiate, e sincronizzare solo quelle riduce le chiamate del 90% e oltre.
Ogni negozio Shopify ha una quantità di «costo» disponibile che si consuma a ogni chiamata API e si ricarica nel tempo. Un client che legge questo valore dalla risposta stessa — invece di indovinare un numero fisso di chiamate al secondo — può rallentare da solo prima di essere bloccato, e sapere quanto aspettare esattamente invece di ritentare alla cieca.
Dando priorità, nella coda di scrittura, ai cali di giacenza a zero rispetto agli aggiornamenti di prezzo. È l'unico tipo di variazione che, se ritardato, genera una vendita su uno stock che non esiste più — e per questo va scritto per primo, non in ordine di arrivo.
GraphQL, per un motivo preciso: dichiara il costo di ogni chiamata e il livello del secchio direttamente nella risposta (extensions.cost.throttleStatus), mentre REST richiede di dedurre il limite dagli header e reagire dopo. Su una sincronizzazione ad alto volume, sapere in anticipo quanto costa una chiamata evita di scoprirlo a suon di errori.